接上文: 一套代码小程序&Web&Native运行的探索02

对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/mvvm

我们在研究如果小程序在多端运行的时候,基本在前端框架这块陷入了困境,因为市面上没有框架可以直接拿来用,而Vue的相识度比较高,而且口碑很好,我们便接着这个机会同步学习Vue也解决我们的问题,我们看看这个系列结束后,会不会离目标进一点,后续如果实现后会重新整理系列文章......

参考:

https://github.com/fastCreator/MVVM(极度参考,十分感谢该作者,直接看Vue会比较吃力的,但是看完这个作者的代码便会轻易很多,可惜这个作者没有对应博客说明,不然就爽了)

https://www.tangshuang.net/3756.html

https://www.cnblogs.com/kidney/p/8018226.html

https://github.com/livoras/blog/issues/13

上文中我们借助HTMLParser这种高级神器,终于将文本中的表达式替换了出来,这里单纯说文本这里也有以下问题:这段是不支持js代码的,+-、三元代码都不支持,所以以上都只是帮助我们理解,还是之前那句话,越是单纯的代码,越是考虑少的代码,可能越是能理解实现,但是后续仍然需要补足,我们这里还是要跟Vue对齐,这样做有个好处,当你不知道怎么做的时候,可以看看Vue的实现,当你思考这么做合不合适的时候,也可以参考Vue,那可是经过烈火淬炼的,值得深度学习,我们今天的任务比较简单便是完整的处理完style、属性以及表达式处理,这里我们直接在fastCreator这个作者下的源码开始学习,还有种学习源码的方法就是抄三次......

我们学习的过程,先将代码写到一起方便理解,后续再慢慢拆分,首先是MVVM类,我们新建libs文件夹,先新建两个js文件,一个html-parser一个index(框架入口文件)

libs--index.js--html-parser.jsindex.html

1 import HTMLParser from './html-parser.js'
2
3 functionarrToObj(arr) {4   let map ={};5   for(let i = 0, l = arr.length; i <  l; i++) {6     map[arr[i].name] =arr[i].value7 }8   returnmap;9 }10
11 functionhtmlParser(html) {12
13   //存储所有节点
14   let nodes =[];15
16   //记录当前节点位置,方便定位parent节点
17   let stack =[];18
19 HTMLParser(html, {20     /*
21 unary: 是不是自闭和标签比如 <br/> input22 attrs为属性的数组23      */
24     start: function( tag, attrs, unary ) { //标签开始
25       /*
26 stack记录的父节点,如果节点长度大于1,一定具有父节点27        */
28       let parent = stack.length ? stack[stack.length - 1] : null;29
30       //最终形成的node对象
31       let node ={32         //1标签, 2需要解析的表达式, 3 纯文本
33         type: 1,34 tag: tag,35 attrs: arrToObj(attrs),36 parent: parent,37         //关键属性
38 children: []39 };40
41       //如果存在父节点,也标志下这个属于其子节点
42       if(parent) {43 parent.children.push(node);44 }45       //还需要处理<br/> <input>这种非闭合标签
46       //...
47
48       //进入节点堆栈,当遇到弹出标签时候弹出
49 stack.push(node)50 nodes.push(node);51
52 //debugger;
53 },54     end: function( tag ) { //标签结束
55       //弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出
56 stack.pop();57
58 //debugger;
59 },60     chars: function( text ) { //文本
61       //如果是空格之类的不予处理
62       if(text.trim() === '') return;63       text =text.trim();64
65       //匹配 {{}} 拿出表达式
66       let reg = /\{\{(.*)\}\}/;67       let node = nodes[nodes.length - 1];68       //如果这里是表达式{{}}需要特殊处理
69       if(!node) return;70
71       if(reg.test(text)) {72 node.children.push({73           type: 2,74           expression: RegExp.$1,75 text: text76 });77       } else{78 node.children.push({79           type: 3,80 text: text81 });82 }83 //debugger;
84 }85 });86
87   returnnodes;88
89 }90
91 export defaultclass MVVM {92   /*
93 暂时要求必须传入data以及el,其他事件什么的不管94
95    */
96 constructor(opts) {97
98     //要求必须存在,这里不做参数校验了
99     this.$el = typeof opts.el === 'string' ?document.getElementById(opts.el) : opts.el;100
101     //data必须存在,其他不做要求
102     this.$data =opts.data;103
104     //模板必须存在
105     this.$template =opts.template;106
107     //存放解析结束的虚拟dom
108     this.$nodes =[];109
110     //将模板解析后,转换为一个函数
111     this.$initRender();112
113     //渲染之
114     this.$render();115     debugger;116 }117
118 $initRender() {119     let template = this.$template;120     let nodes =htmlParser(template);121     this.$nodes =nodes;122 }123
124   //解析模板生成的函数,将最总html结构渲染出来
125 $render() {126
127     let data = this.$data;128     let root = this.$nodes[0];129     let parent = this._createEl(root);130     //简单遍历即可
131
132     this._render(parent, root.children);133
134     this.$el.appendChild(parent);135 }136
137 _createEl(node) {138     let data = this.$data;139
140     let el = document.createElement(node.tag || 'span');141
142     for (let key innode.attrs) {143 el.setAttribute(key, node.attrs[key])144 }145
146     if(node.type === 2) {147       el.innerText =data[node.expression];148     } else if(node.type === 3) {149       el.innerText =node.text;150 }151
152     returnel;153 }154 _render(parent, children) {155     let child = null;156     for(let i = 0, len = children.length; i < len; i++) {157       child = this._createEl(children[i]);158 parent.append(child);159       if(children[i].children) this._render(child, children[i].children);160 }161 }162
163
164 }

index

1 /*
2 * Modified at https://github.com/blowsie/Pure-JavaScript-HTML5-Parser3  */
4
5 //Regular Expressions for parsing tags and attributes
6 let startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,7     endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/,8     attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g9
10 //Empty Elements - HTML 5
11 let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr")12
13 //Block Elements - HTML 5
14 let block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video")15
16 //Inline Elements - HTML 5
17 let inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var")18
19 //Elements that you can, intentionally, leave open
20 //(and which close themselves)
21 let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr")22
23 //Attributes that have their values filled in disabled="disabled"
24 let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected")25
26 //Special Elements (can contain anything)
27 let special = makeMap("script,style")28
29 functionmakeMap(str) {30     var obj = {}, items = str.split(",");31     for (var i = 0; i < items.length; i++)32         obj[items[i]] = true;33     returnobj;34 }35
36 export default functionHTMLParser(html, handler) {37     var index, chars, match, stack = [], last =html;38     stack.last = function() {39         return this[this.length - 1];40 };41
42     while(html) {43         chars = true;44
45         //Make sure we're not in a script or style element
46         if (!stack.last() || !special[stack.last()]) {47
48             //Comment
49             if (html.indexOf("<!--") == 0) {50                 index = html.indexOf("-->");51
52                 if (index >= 0) {53                     if(handler.comment)54                         handler.comment(html.substring(4, index));55                     html = html.substring(index + 3);56                     chars = false;57 }58
59                 //end tag
60             } else if (html.indexOf("</") == 0) {61                 match =html.match(endTag);62
63                 if(match) {64                     html = html.substring(match[0].length);65                     match[0].replace(endTag, parseEndTag);66                     chars = false;67 }68
69                 //start tag
70             } else if (html.indexOf("<") == 0) {71                 match =html.match(startTag);72
73                 if(match) {74                     html = html.substring(match[0].length);75                     match[0].replace(startTag, parseStartTag);76                     chars = false;77 }78 }79
80             if(chars) {81                 index = html.indexOf("<");82
83                 var text = index < 0 ? html : html.substring(0, index);84                 html = index < 0 ? "": html.substring(index);85
86                 if(handler.chars)87 handler.chars(text);88 }89
90         } else{91             html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function(all, text) {92                 text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2");93                 if(handler.chars)94 handler.chars(text);95
96                 return "";97 });98
99             parseEndTag("", stack.last());100 }101
102         if (html ==last)103             throw "Parse Error: " +html;104         last =html;105 }106
107     //Clean up any remaining tags
108 parseEndTag();109
110     functionparseStartTag(tag, tagName, rest, unary) {111         tagName =tagName.toLowerCase();112
113         if(block[tagName]) {114             while (stack.last() &&inline[stack.last()]) {115                 parseEndTag("", stack.last());116 }117 }118
119         if (closeSelf[tagName] && stack.last() ==tagName) {120             parseEndTag("", tagName);121 }122
123         unary = empty[tagName] || !!unary;124
125         if (!unary)126 stack.push(tagName);127
128         if(handler.start) {129             var attrs =[];130
131             rest.replace(attr, function(match, name) {132                 var value = arguments[2] ? arguments[2] :133                     arguments[3] ? arguments[3] :134                         arguments[4] ? arguments[4] :135                             fillAttrs[name] ? name : "";136
137 attrs.push({138 name: name,139 value: value,140                     escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
141 });142 });143
144             if(handler.start)145 handler.start(tagName, attrs, unary);146 }147 }148
149     functionparseEndTag(tag, tagName) {150         //If no tag name is provided, clean shop
151         if (!tagName)152             var pos = 0;153
154         //Find the closest opened tag of the same type
155         else
156             for (var pos = stack.length - 1; pos >= 0; pos--)157                 if (stack[pos] ==tagName)158                     break;159
160         if (pos >= 0) {161             //Close all the open elements, up the stack
162             for (var i = stack.length - 1; i >= pos; i--)163                 if(handler.end)164 handler.end(stack[i]);165
166             //Remove the open elements from the stack
167             stack.length =pos;168 }169 }170 };

html-parser

这个时候我们的index代码量便下来了:

1 <!doctype html>
2 <html>
3 <head>
4   <title>起步</title>
5 </head>
6 <body>
7
8 <divid="app">
9
10 </div>
11
12 <scripttype="module">
13
14 import MVVM from'./libs/index.js'
15
16 let html=`17 <div class="c-row search-line"data-flag="start"ontap="clickHandler">
18   <div class="c-span9 js-start search-line-txt">
19 {{name}}</div>
20     <input type="text">
21      <br>
22 </div>
23 `24
25 let vm= newMVVM({26 el:'app',27 template: html,28 data: {29 name:'叶小钗'
30 }31 })32
33 </script>
34 </body>
35 </html>

我们现在来更改index.js入口文件的代码,这里特别说一下其中的$mount方法,他试试是要做一个这样的事情:

//模板字符串<divid= "app">{{message}}</div>

//render函数
functionanonymous() {with(this){return _h('div',{attrs:{"id":"app"}},["\n  "+_s(message)+"\n"])}
}

将模板转换为一个函数render放到参数上,这里我们先简单实现,后续深入后我们重新翻下这个函数,修改后我们的index.js变成了这个样子:

1 import HTMLParser from './html-parser.js'
2
3
4 //工具函数 begin
5
6 functionisFunction(obj) {7   return typeof obj === 'function'
8 }9
10
11 functionmakeAttrsMap(attrs, delimiters) {12   const map ={}13   for (let i = 0, l = attrs.length; i < l; i++) {14     map[attrs[i].name] =attrs[i].value;15 }16   returnmap;17 }18
19
20
21 //dom操作
22 functionquery(el) {23   if (typeof el === 'string') {24     const selector =el25     el =document.querySelector(el)26     if (!el) {27       return document.createElement('div')28 }29 }30   returnel31 }32
33 functioncached(fn) {34   const cache = Object.create(null)35   return functioncachedFn(str) {36     const hit =cache[str]37     return hit || (cache[str] =fn(str))38 }39 }40
41 let idToTemplate = cached(function(id) {42   var el =query(id)43   return el &&el.innerHTML;44 })45
46
47
48 //工具函数 end
49
50 //模板解析函数 begin
51
52 const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g53 const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g
54
55 const buildRegex = cached(delimiters =>{56     const open = delimiters[0].replace(regexEscapeRE, '\\$&')57     const close = delimiters[1].replace(regexEscapeRE, '\\$&')58     return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')59 })60
61
62 functionTextParser(text, delimiters) {63   const tagRE = delimiters ?buildRegex(delimiters) : defaultTagRE64   if (!tagRE.test(text)) {65     return
66 }67   const tokens =[]68   let lastIndex = tagRE.lastIndex = 0
69 let match, index70   while ((match =tagRE.exec(text))) {71     index =match.index72     //push text token
73     if (index >lastIndex) {74 tokens.push(JSON.stringify(text.slice(lastIndex, index)))75 }76     //tag token
77     const exp = match[1].trim()78 tokens.push(`_s(${exp})`)79     lastIndex = index + match[0].length80 }81   if (lastIndex <text.length) {82 tokens.push(JSON.stringify(text.slice(lastIndex)))83 }84   return tokens.join('+')85 }86
87 //******核心中的核心
88 functioncompileToFunctions(template, vm) {89 let root;90 let currentParent;91   let options =vm.$options;92   let stack =[];93
94   //这段代码昨天做过解释,这里属性参数比昨天多一些
95 HTMLParser(template, {96     start: function(tag, attrs, unary) {97
98       let element ={99 vm: vm,100         //1 标签 2 文本表达式 3 文本
101         type: 1,102 tag,103         //数组
104 attrsList: attrs,105         attrsMap: makeAttrsMap(attrs), //将属性数组转换为对象
106 parent: currentParent,107 children: []108 };109
110       if(!root) {111         vm.$vnode = root =element;112 }113
114       if(currentParent && !element.forbidden) {115 currentParent.children.push(element);116         element.parent =currentParent;117 }118
119       if(!unary) {120         currentParent =element;121 stack.push(element);122 }123
124 },125     end: function(tag) {126       //获取当前元素
127       let element = stack[stack.length - 1];128       let lastNode = element.children[element.children.length - 1];129       //删除最后一个空白节点,暂时感觉没撒用呢
130       if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') {131 element.children.pop();132 }133
134       //据说比调用pop节约性能相当于stack.pop()
135       stack.length -= 1;136       currentParent = stack[stack.length - 1];137
138 },139     //处理真实的节点
140     chars: function(text) {141       if (!text.trim()) {142         //text = ' '
143         return;144 }145       //解析文本节点 exp: a{{b}}c => 'a'+_s(a)+'b'
146       let expression =TextParser(text, options.delimiters)147       if(expression) {148 currentParent.children.push({149           type: 2,150 expression,151 text152 })153       } else{154         currentParent &&currentParent.children.push({155           type: 3,156 text157 })158 }159 }160
161 });162
163   returnroot;164
165 }166
167
168 //模板解析函数 end
169
170 //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染
171 //要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说
172 functioninitData(vm, data) {173   if(isFunction(data)) {174     data =data()175 }176   vm.$data =data;177 }178
179 //全局数据保证每个MVVM实例拥有唯一id
180 let uid = 0;181
182 export defaultclass MVVM {183 constructor(options) {184     this.$options =options;185
186     //我们可以在传入参数的地方设置标签替换方式,比如可以设置为['<%=', '%>'],注意这里是数组
187     this.$options.delimiters = this.$options.delimiters || ["{{", "}}"];188
189     //唯一标志
190     this._uid = uid++;191
192     if(options.data) {193       //194       initData(this, options.data);195 }196
197     this.$mount(options.el);198
199 }200
201   //解析模板compileToFunctions,将之形成一个函数
202   //很多网上的解释是将实例挂载到dom上,这里有些没明白,我们后面点再看看
203 $mount(el) {204     let options = this.$options;205
206     el = el &&query(el);207     this.$el =el;208
209     //如果用户自定义了render函数则不需要解析template
210     //这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode
211     if(!options.render) {212       let  template =options.template;213       if(template) {214         if(typeof template === 'string') {215           //获取script的template模板
216           if (template[0] === '#') {217             template =idToTemplate(template)218 }219         } else if(template.nodeType) {220           //如果template是个dom结构,只能有一个根节点
221           template =template.innerHTML;222 }223 }224
225       //上面的代码什么都没做,只是确保正确的拿到了template数据,考虑了各种情况
226       //下面这段是关键,也是我们昨天干的事情
227       if(template) {228         //***核心函数***/
229         let render = compileToFunctions(template, this);230         options.render =render;231 }232
233
234 }235
236
237
238 }239
240
241 }242
243 //过去的代码
244 functionarrToObj(arr) {245   let map ={};246   for(let i = 0, l = arr.length; i <  l; i++) {247     map[arr[i].name] =arr[i].value248 }249   returnmap;250 }251
252 functionhtmlParser(html) {253
254   //存储所有节点
255   let nodes =[];256
257   //记录当前节点位置,方便定位parent节点
258   let stack =[];259
260 HTMLParser(html, {261     /*
262 unary: 是不是自闭和标签比如 <br/> input263 attrs为属性的数组264      */
265     start: function( tag, attrs, unary ) { //标签开始
266       /*
267 stack记录的父节点,如果节点长度大于1,一定具有父节点268        */
269       let parent = stack.length ? stack[stack.length - 1] : null;270
271       //最终形成的node对象
272       let node ={273         //1标签, 2需要解析的表达式, 3 纯文本
274         type: 1,275 tag: tag,276 attrs: arrToObj(attrs),277 parent: parent,278         //关键属性
279 children: []280 };281
282       //如果存在父节点,也标志下这个属于其子节点
283       if(parent) {284 parent.children.push(node);285 }286       //还需要处理<br/> <input>这种非闭合标签
287       //...
288
289       //进入节点堆栈,当遇到弹出标签时候弹出
290 stack.push(node)291 nodes.push(node);292
293 //debugger;
294 },295     end: function( tag ) { //标签结束
296       //弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出
297 stack.pop();298
299 //debugger;
300 },301     chars: function( text ) { //文本
302       //如果是空格之类的不予处理
303       if(text.trim() === '') return;304       text =text.trim();305
306       //匹配 {{}} 拿出表达式
307       let reg = /\{\{(.*)\}\}/;308       let node = nodes[nodes.length - 1];309       //如果这里是表达式{{}}需要特殊处理
310       if(!node) return;311
312       if(reg.test(text)) {313 node.children.push({314           type: 2,315           expression: RegExp.$1,316 text: text317 });318       } else{319 node.children.push({320           type: 3,321 text: text322 });323 }324 //debugger;
325 }326 });327
328   returnnodes;329
330 }331
332 class MVVM1 {333   /*
334 暂时要求必须传入data以及el,其他事件什么的不管335
336    */
337 constructor(opts) {338
339     //要求必须存在,这里不做参数校验了
340     this.$el = typeof opts.el === 'string' ?document.getElementById(opts.el) : opts.el;341
342     //data必须存在,其他不做要求
343     this.$data =opts.data;344
345     //模板必须存在
346     this.$template =opts.template;347
348     //存放解析结束的虚拟dom
349     this.$nodes =[];350
351     //将模板解析后,转换为一个函数
352     this.$initRender();353
354     //渲染之
355     this.$render();356     debugger;357 }358
359 $initRender() {360     let template = this.$template;361     let nodes =htmlParser(template);362     this.$nodes =nodes;363 }364
365   //解析模板生成的函数,将最总html结构渲染出来
366 $render() {367
368     let data = this.$data;369     let root = this.$nodes[0];370     let parent = this._createEl(root);371     //简单遍历即可
372
373     this._render(parent, root.children);374
375     this.$el.appendChild(parent);376 }377
378 _createEl(node) {379     let data = this.$data;380
381     let el = document.createElement(node.tag || 'span');382
383     for (let key innode.attrs) {384 el.setAttribute(key, node.attrs[key])385 }386
387     if(node.type === 2) {388       el.innerText =data[node.expression];389     } else if(node.type === 3) {390       el.innerText =node.text;391 }392
393     returnel;394 }395 _render(parent, children) {396     let child = null;397     for(let i = 0, len = children.length; i < len; i++) {398       child = this._createEl(children[i]);399 parent.append(child);400       if(children[i].children) this._render(child, children[i].children);401 }402 }403
404
405 }

index.js

这里仅仅是到输出vnode这步,接下来是将vnode转换为函数render,在写这段代码之前我们来说一说Vue中的render参数,事实上,我们new Vue的时候可以直接传递render参数:

1 newVue({2     render: function() {3         return this._h('div', {4 attrs:{5                 a: 'aaa'
6 }7 }, [8            this._h('div')9 ])10 }11 })

他对应的这段代码:

1 newVue({2     template: '<div class="aa">Hello World! </div>'
3 })

真实代码过程中的过程,以及我们上面代码的过程是,template 字符串 => 虚拟DOM对象 ast => 根据ast生成render函数......,这里又涉及到了另一个需要引用的工具库snabbdom

snabbdom-render

https://github.com/snabbdom/snabbdom,Vue2.0底层借鉴了snabdom,我们这里先重点介绍他的h函数,h(help帮助创建vnode)函数可以让我们轻松创建vnode,这里再对Virtual DOM做一个说明,这段话是我看到觉得很好的解释的话(https://github.com/livoras/blog/issues/13):

我们一段js对象可以很容易的翻译为一段HTML代码:

1 var element ={2   tagName: 'ul', //节点标签名
3   props: { //DOM的属性,用一个对象存储键值对
4     id: 'list'
5 },6   children: [ //该节点的子节点
7     {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},8     {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},9     {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},10 ]11 }

1 <ulid='list'>
2   <liclass='item'>Item 1</li>
3   <liclass='item'>Item 2</li>
4   <liclass='item'>Item 3</li>
5 </ul>

同样的,我们一段HTML代码其实属性、参数是很有限的,也十分轻易的能转换成一个js对象,我们如果使用dom操作改变了我们的html结构,事实上会形成一个新的js对象,这个时候我们将渲染后形成的js对象和渲染前形成的js对象进行对比,便可以清晰知道这次变化的差异部分,然后拿着差异部分的js对象(每个js对象都会映射到一个真实的dom对象)做更新即可,关于Virtual DOM文章作者对此做了一个总结:

① 用js对象表示DOM树结构,然后用这个js对象树结构生成一个真正的DOM树(document.create***操作),插入文档中(这个时候会形成render tree,看得到了)

② 当状态变化时(数据变化时),重新构造一颗新的对象树,和之前的作对比,记录差异部分

③ 将差异部分的数据更新到视图上,更新结束

他这里描述的比较简单,事实上我们根据昨天的学习,可以知道框架事实上是劫持了没个数据对象,所以每个数据对象做了改变,会影响到哪些DOM结构是有记录的,这块我们后面章节再说,我们其实今天主要的目的还是处理文本和属性生成,却不想提前接触虚拟DOM了......

其实我们之前的js对象element就已经可以代表一个虚拟dom了,之所以引入snabbddom应该是后面要处理diff部分,所以我们乖乖的学吧,首先我们定义一个节点的类:

1 class Element {2 constructor(tagName, props, children) {3     this.tagName =tagName;4     this.props =props;5     this.children =children;6 }7 }

上面的dom结构便可以变成这样了:

1 new Element('ul', {id: 'list'}, [2   new Element('li', {class: 'item'}, ['Item 1']),3   new Element('li', {class: 'item'}, ['Item 2']),4   new Element('li', {class: 'item'}, ['Item 3'])5 ])

似乎代码有点不好看,于是封装下实例化操作:

1 class Element {2 constructor(tagName, props, children) {3     this.tagName =tagName;4     this.props =props;5     this.children =children;6 }7 }8
9 functionel(tagName, props, children)  {10   return newElement(tagName, props, children)11 }12
13 el('ul', {id: 'list'}, [14   el('li', {class: 'item'}, ['Item 1']),15   el('li', {class: 'item'}, ['Item 2']),16   el('li', {class: 'item'}, ['Item 3'])17 ])

然后就是根据这个js对象生成真正的DOM结构,也就是上面的html字符串:

1 <!doctype html>
2 <html>
3 <head>
4   <title>起步</title>
5 </head>
6 <body>
7
8 <scripttype="text/javascript">
9   //***虚拟dom部分代码,后续会换成snabdom
10 class Element {11 constructor(tagName, props, children) {12       this.tagName=tagName;13       this.props=props;14       this.children=children;15 }16 render() {17       //拿着根节点往下面撸
18 let root=document.createElement(this.tagName);19 let props= this.props;20
21       for(let nameinprops) {22 root.setAttribute(name, props[name]);23 }24
25 let children= this.children;26
27       for(let i= 0, l=children.length; i<l; i++) {28 let child=children[i];29 let childEl;30         if(childinstanceofElement) {31           //递归调用
32 childEl=child.render();33 }else{34 childEl=document.createTextNode(child);35 }36 root.append(childEl);37 }38
39       this.rootNode=root;40       returnroot;41 }42 }43
44   functionel(tagName, props, children)  {45     return newElement(tagName, props, children)46 }47
48 let vnode=el('ul', {id:'list'}, [49 el('li', {class:'item'}, ['Item 1']),50 el('li', {class:'item'}, ['Item 2']),51 el('li', {class:'item'}, ['Item 3'])52 ])53
54 let root=vnode.render();55
56 document.body.appendChild(root);57
58 </script>
59
60 </body>
61 </html>

饶了这么大一圈子,我们再回头看这段代码:

1 newVue({2     render: function() {3         return this._h('div', {4 attrs:{5                 a: 'aaa'
6 }7 }, [8            this._h('div')9 ])10 }11 })

这个时候,我们对这个_h干了什么,可能便有比较清晰的认识了,于是我们回到我们之前的代码,暂时跳出snabbdom

解析模板

在render中,我们有这么一段代码:

1 //没有指令时运行,或者指令解析完毕
2 functionnodir(el) {3 let code4   //设置属性 等值
5   const data =genData(el);6   //转换子节点
7   const children = genChildren(el, true);8   code = `_h('${el.tag}'${9         data ? `,${data}` : '' //data
10 }${11         children ? `,${children}` : '' //children
12 })`13 returncode14 }

事实上这个跟上面那坨代码完成的工作差不多(同样的遍历加递归),只不过他这里还有更多的目的,比如这段代码最终会生成这样的:

_h('div',{},[_h('div',{},["\n    "+_s(name)]),_h('input',{}),_h('br',{})])

这段代码会被包装成一个模板类,等待被实例化,显然到这里还没进入我们的模板解析过程,因为里面出现了_s(name),我们如果加一个span的话会变成这样:

1 <divclass="c-row search-line"data-flag="start"ontap="clickHandler">
2   <divclass="c-span9 js-start search-line-txt">
3     {{name}}</div>
4     <span>{{age+1}}</span>
5     <inputtype="text">
6      <br>
7 </div>

_h('div',{},[_h('div',{},["\n    "+_s(name)]),_h('span',{},[_s(age+1)]),_h('input',{}),_h('br',{})])

真实运行的时候这段代码是这个样子的:

这段代码很纯粹,不包含属性和class,我们只需要处理文本内容替换即可,今天的任务比较简单,所以接下来的流程后便可以得出第一阶段代码:

1 <!doctype html>
2 <html>
3 <head>
4   <title>起步</title>
5 </head>
6 <body>
7
8 <divid="app">
9
10 </div>
11
12 <scripttype="module">
13
14 import MVVM from'./libs/index.js'
15
16 let html=`17 <div class="c-row search-line"data-flag="start"ontap="clickHandler">
18   <div class="c-span9 js-start search-line-txt">
19 {{name}}</div>
20     <span>{{age+1}}</span>
21     <input type="text">
22      <br>
23 </div>
24 `25
26 let vm= newMVVM({27 el:'#app',28 template: html,29 data: {30 name:'叶小钗',31 age:30
32 }33 })34
35 </script>
36 </body>
37 </html>

1 import HTMLParser from './html-parser.js'
2
3
4 //工具函数 begin
5
6 functionisFunction(obj) {7   return typeof obj === 'function'
8 }9
10
11 functionmakeAttrsMap(attrs, delimiters) {12   const map ={}13   for (let i = 0, l = attrs.length; i < l; i++) {14     map[attrs[i].name] =attrs[i].value;15 }16   returnmap;17 }18
19
20
21 //dom操作
22 functionquery(el) {23   if (typeof el === 'string') {24     const selector =el25     el =document.querySelector(el)26     if (!el) {27       return document.createElement('div')28 }29 }30   returnel31 }32
33 functioncached(fn) {34   const cache = Object.create(null)35   return functioncachedFn(str) {36     const hit =cache[str]37     return hit || (cache[str] =fn(str))38 }39 }40
41 let idToTemplate = cached(function(id) {42   var el =query(id)43   return el &&el.innerHTML;44 })45
46
47
48 //工具函数 end
49
50 //模板解析函数 begin
51
52 const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g53 const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g
54
55 const buildRegex = cached(delimiters =>{56     const open = delimiters[0].replace(regexEscapeRE, '\\$&')57     const close = delimiters[1].replace(regexEscapeRE, '\\$&')58     return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')59 })60
61
62 functionTextParser(text, delimiters) {63   const tagRE = delimiters ?buildRegex(delimiters) : defaultTagRE64   if (!tagRE.test(text)) {65     return
66 }67   const tokens =[]68   let lastIndex = tagRE.lastIndex = 0
69 let match, index70   while ((match =tagRE.exec(text))) {71     index =match.index72     //push text token
73     if (index >lastIndex) {74 tokens.push(JSON.stringify(text.slice(lastIndex, index)))75 }76     //tag token
77     const exp = match[1].trim()78 tokens.push(`_s(${exp})`)79     lastIndex = index + match[0].length80 }81   if (lastIndex <text.length) {82 tokens.push(JSON.stringify(text.slice(lastIndex)))83 }84   return tokens.join('+')85 }86
87 functionmakeFunction(code) {88   try{89     return newFunction(code)90   } catch(e) {91     return function(){};92 }93 }94
95 //***虚拟dom部分代码,后续会换成snabdom
96 class Element {97 constructor(tagName, props, children) {98     this.tagName =tagName;99     this.props =props;100     this.children = children ||[];101 }102 render() {103     //拿着根节点往下面撸
104     let el = document.createElement(this.tagName);105     let props = this.props;106
107     for(let name inprops) {108 el.setAttribute(name, props[name]);109 }110
111     let children = this.children;112
113     for(let i = 0, l = children.length; i < l; i++) {114       let child =children[i];115 let childEl;116       if(child instanceofElement) {117         //递归调用
118         childEl =child.render();119       } else{120         childEl =document.createTextNode(child);121 }122 el.append(childEl);123 }124     returnel;125 }126 }127
128 functionel(tagName, props, children)  {129   return newElement(tagName, props, children)130 }131
132 //***核心中的核心,将vnode转换为函数
133
134 const simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/
135 const modifierCode ={136   stop: '$event.stopPropagation();',137   prevent: '$event.preventDefault();',138   self: 'if($event.target !== $event.currentTarget)return;',139   ctrl: 'if(!$event.ctrlKey)return;',140   shift: 'if(!$event.shiftKey)return;',141   alt: 'if(!$event.altKey)return;',142   meta: 'if(!$event.metaKey)return;'
143 }144
145 const keyCodes ={146   esc: 27,147   tab: 9,148   enter: 13,149   space: 32,150   up: 38,151   left: 37,152   right: 39,153   down: 40,154   'delete': [8, 46]155 }156
157
158 functioncodeGen(ast) {159   //解析成h render字符串形式
160   const code = ast ? genElement(ast) : '_h("div")'
161   //把render函数,包起来,使其在当前作用域内
162   return makeFunction(`with(this){ debugger; return${code}}`)163 }164
165 functiongenElement(el) {166   //无指令
167   returnnodir(el)168 }169
170 //没有指令时运行,或者指令解析完毕
171 functionnodir(el) {172 let code173   //设置属性 等值
174   const data =genData(el);175   //转换子节点
176   const children = genChildren(el, true);177   code = `_h('${el.tag}'${178         data ? `,${data}` : '' //data
179 }${180         children ? `,${children}` : '' //children
181 })`182 returncode183 }184
185 functiongenChildren(el, checkSkip) {186   const children =el.children187   if(children.length) {188     const el = children[0]189     //如果是v-for
190     //if (children.length === 1 && el.for) {
191     //return genElement(el)
192     //}
193     const normalizationType = 0
194     return `[${children.map(genNode).join(',')}]${195 checkSkip196                 ? normalizationType ? `,${normalizationType}` : ''
197   : ''
198 }`199 }200 }201
202 functiongenNode(node) {203   if (node.type === 1) {204     returngenElement(node)205   } else{206     returngenText(node)207 }208 }209
210 functiongenText(text) {211   return text.type === 2 ?text.expression : JSON.stringify(text.text)212 }213
214 functiongenData(el) {215   let data = '{'
216   //attributes
217   if(el.style) {218     data += 'style:' + genProps(el.style) + ','
219 }220   if(Object.keys(el.attrs).length) {221     data += 'attrs:' + genProps(el.attrs) + ','
222 }223   if(Object.keys(el.props).length) {224     data += 'props:' + genProps(el.props) + ','
225 }226   if(Object.keys(el.events).length) {227     data += 'on:' + genProps(el.events) + ','
228 }229   if(Object.keys(el.hook).length) {230     data += 'hook:' + genProps(el.hook) + ','
231 }232   data = data.replace(/,$/, '') + '}'
233   returndata234 }235
236 functiongenProps(props) {237   let res = '{';238   for (let key inprops) {239     res += `"${key}":${props[key]},`240 }241   return res.slice(0, -1) + '}'
242 }243
244 //******核心中的核心
245 functioncompileToFunctions(template, vm) {246 let root;247 let currentParent;248   let options =vm.$options;249   let stack =[];250
251   //这段代码昨天做过解释,这里属性参数比昨天多一些
252 HTMLParser(template, {253     start: function(tag, attrs, unary) {254
255       let element ={256 vm: vm,257         //1 标签 2 文本表达式 3 文本
258         type: 1,259 tag,260         //数组
261 attrsList: attrs,262         attrsMap: makeAttrsMap(attrs), //将属性数组转换为对象
263 parent: currentParent,264 children: [],265
266         //下面这些属性先不予关注,因为底层函数没有做校验,不传要报错
267 events: {},268         style: null,269 hook: {},270         props: {},//DOM属性
271         attrs: {}//值为true,false则移除该属性
272
273 };274
275       if(!root) {276         vm.$vnode = root =element;277 }278
279       if(currentParent && !element.forbidden) {280 currentParent.children.push(element);281         element.parent =currentParent;282 }283
284       if(!unary) {285         currentParent =element;286 stack.push(element);287 }288
289 },290     end: function(tag) {291       //获取当前元素
292       let element = stack[stack.length - 1];293       let lastNode = element.children[element.children.length - 1];294       //删除最后一个空白节点,暂时感觉没撒用呢
295       if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') {296 element.children.pop();297 }298
299       //据说比调用pop节约性能相当于stack.pop()
300       stack.length -= 1;301       currentParent = stack[stack.length - 1];302
303 },304     //处理真实的节点
305     chars: function(text) {306       if (!text.trim()) {307         //text = ' '
308         return;309 }310       //解析文本节点 exp: a{{b}}c => 'a'+_s(a)+'b'
311       let expression =TextParser(text, options.delimiters)312       if(expression) {313 currentParent.children.push({314           type: 2,315 expression,316 text317 })318       } else{319         currentParent &&currentParent.children.push({320           type: 3,321 text322 })323 }324 }325
326 });327
328   //***关键代码***
329   //将vnode转换为render函数,事实上可以直接传入这种render函数,便不会执行这块逻辑,编译时候会把这块工作做掉
330   returncodeGen(root);331
332 }333
334
335 //模板解析函数 end
336
337 //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染
338 //要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说
339 functioninitData(vm, data) {340   if(isFunction(data)) {341     data =data()342 }343
344   //这里将data上的数据移植到this上,后面要监控
345   for(let key indata) {346
347     //这里有可能会把自身方法覆盖,所以自身的属性方法需要+$
348     vm[key] =data[key];349 }350
351   vm.$data =data;352 }353
354 //全局数据保证每个MVVM实例拥有唯一id
355 let uid = 0;356
357 export defaultclass MVVM {358 constructor(options) {359     this.$options =options;360
361     //我们可以在传入参数的地方设置标签替换方式,比如可以设置为['<%=', '%>'],注意这里是数组
362     this.$options.delimiters = this.$options.delimiters || ["{{", "}}"];363
364     //唯一标志
365     this._uid = uid++;366
367     if(options.data) {368       //369       initData(this, options.data);370 }371
372     this.$mount(options.el);373
374     let _node = this._render().render();375     this.$el.appendChild( _node)376
377 }378
379   //解析模板compileToFunctions,将之形成一个函数
380   //很多网上的解释是将实例挂载到dom上,这里有些没明白,我们后面点再看看
381 $mount(el) {382     let options = this.$options;383
384     el = el &&query(el);385     this.$el =el;386
387     //如果用户自定义了render函数则不需要解析template
388     //这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode
389     if(!options.render) {390       let  template =options.template;391       if(template) {392         if(typeof template === 'string') {393           //获取script的template模板
394           if (template[0] === '#') {395             template =idToTemplate(template)396 }397         } else if(template.nodeType) {398           //如果template是个dom结构,只能有一个根节点
399           template =template.innerHTML;400 }401 }402
403       //上面的代码什么都没做,只是确保正确的拿到了template数据,考虑了各种情况
404       //下面这段是关键,也是我们昨天干的事情
405       if(template) {406         //***核心函数***/
407         let render = compileToFunctions(template, this);408         options.render =render;409 }410 }411
412     return this;413 }414
415 _render() {416     let render = this.$options.render417 let vnode418     try{419       //自动解析的template不需要h,用户自定义的函数需要h
420       vnode = render.call(this, this._h);421     } catch(e) {422 warn(`render Error : ${e}`)423 }424     returnvnode425 }426
427 _h(tag, data, children) {428     returnel(tag, data, children)429 }430
431 _s(val) {432     return val == null
433       ? ''
434       : typeof val === 'object'
435       ? JSON.stringify(val, null, 2)436 : String(val)437 }438
439 }

libs/index.js

之前我们图简单,一直没有解决属性问题,现在我们在模板里面加入一些属性:

1 <div class="c-row search-line" data-name="{{name}}" data-flag="start" ontap="clickHandler">
2   <div class="c-span9 js-start search-line-txt">
3     {{name}}</div>
4     <span>{{age+1}}</span>
5     <input type="text" value="{{age}}">
6      <br>
7 </div>

情况就变得有所不同了,这里多加一句:

 1 setElAttrs(el, delimiters)2 //==>3 function setElAttrs(el, delimiters) {4   var s = delimiters[0], e = delimiters[1];5   var reg = new RegExp(`^${s}(\.+\)${e}$`);6   var attrs = el.attrsMap;7   for (let key in attrs) { 8 let value = attrs[key]; 9 var match = value.match(reg) 10 if (match) { 11 value = match[1]; 12 if (isAttr(key)) { 13 el.props[key] = '_s('+value+')'; 14 } else { 15 el.attrs[key] = value; 16  } 17 } else { 18 if (isAttr(key)) { 19 el.props[key] = "'" + value + "'"; 20 } else { 21 el.attrs[key] = "'" + value + "'"; 22  } 23  } 24 25  } 26 }

这段代码会处理所有的属性,如果是属性中包含“{{}}”关键词,便会替换,不是我们的属性便放到attrs中,是的就放到props中,这里暂时不太能区分为什么要分为attrs何props,后续我们这边给出代码,于是我们的index.js变成了这个样子:

libs/index.js
_h('div',{attrs:{"data-name":name,"data-flag":'start',"ontap":'clickHandler'},props:{"class":'c-row search-line'}},[_h('div',{props:{"class":'c-span9 js-start search-line-txt'}},["\n    "+_s(name)]),_h('span',{},[_s(age+1)]),_h('input',{props:{"type":'text',"value":_s(age)}}),_h('br',{})])

1 <div id="app">
2   <div class="c-row search-line" data-name="叶小钗" data-flag="start" ontap="clickHandler">
3     <div class="c-span9 js-start search-line-txt">
4       叶小钗</div>
5     <span>31</span>
6     <input type="text" value="30">
7     <br>
8   </div>
9 </div>

然后我们来处理class以及style,他们是需要特殊处理的:

<div class="c-row search-line {{name}} {{age}}" style="font-size: 14px; margin-left: {{age}}px " data-name="{{name}}" data-flag="start" ontap="clickHandler"><div class="c-span9 js-start search-line-txt">{{name}}</div><span>{{age+1}}</span><input type="text" value="{{age}}"><br>
</div>

libs/index.js

生成了如下代码:

1 <div class="c-row search-line 叶小钗 30" data-name="叶小钗" data-flag="start" ontap="clickHandler" style="font-size:  14px; margin-left:  30px ;">
2   <div class="c-span9 js-start search-line-txt">
3     叶小钗</div>
4   <span>31</span>
5   <input type="text" value="30">
6   <br>
7 </div>

虽然这段代码能运行,无论如何我们的属性和class也展示出来了,但是问题却不少:

① 这段代码仅仅就是为了运行,或者说帮助我们理解

② libs/index.js代码已经超过了500行,维护起来有点困难了,连我自己都有时候找不到东西,所以我们该分拆文件了

于是,我们暂且忍受这段说明性(演示性)代码,将之进行文件分拆

文件分拆

文件拆分后代码顺便传到了github上:https://github.com/yexiaochai/wxdemo/tree/master/mvvm

这里简单的解释下各个文件是干撒的:

 1 ./libs2 ..../codegen.js 代码生成器,传入一个ast(js树对象),转换为render函数3 ..../helps.js 处理vnode的相关工具函数,比如处理属性节点,里面的生成函数感觉该放到utils中4 ..../html-parser.js 第三方库,HTML解析神器,帮助生成js dom树对象5 ..../instance.js 初始化mvvm实例工具类6 ..../mvvm.js 入口函数7 ..../parser.js 模板解析生成render函数,核心8 ..../text-parser.js 工具类,将{{}}做替换生成字符串9 ..../utils.js 工具库
10 ..../vnode.js 虚拟树库,暂时自己写的,后续要换成snabbdom
11 ./index.html 入口文件

今天的学习到此位置,明天我们来处理数据更新相关

转载于:https://www.cnblogs.com/yexiaochai/p/9699698.html

一套代码小程序WebNative运行的探索03——处理模板及属性相关推荐

  1. 一套代码小程序WebNative运行的探索02

    接上文:一套代码小程序&Web&Native运行的探索01,本文都是一些探索性为目的的研究学习,在最终版输出前,内中的内容可能会有点乱 参考: https://github.com/f ...

  2. 一套代码小程序WebNative运行的探索01

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 并且用小程序翻写了之前一个demo:[组件化开发]前端进阶篇之如何编写可维护可升级的代码 之前一直在跟业务方打交道 ...

  3. 【一套代码小程序NativeWeb阶段总结篇】可以这样阅读Vue源码

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...

  4. 1000套微信小程序源码源代码带后台带运行截图预览图学习资料网盘下载

    1000多套微信小程序源码带后台+教程+不同行业的源码集合 o2o行业 | - 盒马鲜生 | - 轻客洗衣 互联网行业 | - 云文档 | - 仿ofo共享单车 | - 仿美团外卖 | - 仿饿了么 ...

  5. uni app 调用网络打印机_一套代码,七端运行-uni-app

    为什么要使用uni-app uni-app 是一个使用 Vue.js 开发跨平台应用的前端框架,开发者编写一套代码,可编译到iOS.Android.H5.小程序等多个平台. 同时在别的方面它也有很大优 ...

  6. 免费分享20套微信小程序源码 源码免费下载【强烈推荐】

    淘源码:国内知名的源码免费下载平台 微信小程序源码包括:商城系统源码.点餐外卖源码.垃圾分类识别源码.预约洗车源码.物业管理源码.校园跑腿源码.驾考学习源码.会议预约源码.图书管理源码.智能停车源码. ...

  7. 阿里云ARMS重磅推出小程序监控,助力小程序稳定运行

    2018年是小程序蓬勃发展的一年,各大公司如腾讯.阿里.百度.头条等都陆续推出了自己的小程序,小程序已成为一个未来必然的趋势.移动互联网的新风口.据数据统计,目前已上线的微信小程序已超过100万,支付 ...

  8. 使用uniapp做微信小程序,在小程序编辑器运行时编译报错:appid不合法,导致启用不了。

    使用uniapp做微信小程序,在小程序编辑器运行时编译报错:appid不合法,导致启用不了.这个好坑爹啊,我这么淑女的小可爱都想骂人了,我好累,写个博客发泄发泄吧! 复现我的问题:神操作--代码跟别人 ...

  9. uniapp写微信小程序怎么运行到微信开发工具上

    1.选择运行>运行到小程序模拟器>运行到微信开发者工具 2.这样unpackage中就多一个文件mp-weixin 3.把这个文件导入到微信小程序中就行啦

最新文章

  1. Tomcat:Connection reset by peer: socket write error
  2. 多天线技术是LTE的重要演进方向已成为产业共识
  3. Server Error in '/' Application. 报错
  4. Oracle使用impdb/expdb数据泵导入/导出数据库
  5. android给后台传递json,将服务中的JSON数据发送到Android中的UI
  6. android 6.0版本名字,棉花糖Marshmallow 是Android 6.0的名字
  7. 企业微信H5_消息推送概述,发送应用消息示例
  8. 数据字符集mysql主从数据库,分库分表等笔记
  9. mysql begin end 用法_数据库:Mysql中“select ... for update”排他锁分析
  10. 【网络】HTTPS 怎么保证数据传输的安全性
  11. java 重载 调用指定_java 方法重载的时候,同一个类,父子类,调用哪个方法的问题...
  12. jquery手机横屏竖屏判断显示
  13. sql 时间字符串转换
  14. Atitit 价值观与理念总结 Diy自力更生尽可能 提高独立性 减少依赖 大而全优先于小而美 适度分权防止集权导致大决策失误 方式多种多样 综合是王道 简单快速优先 努力实现人们喜闻乐见的服务 信
  15. SLAM--卡尔曼滤波、粒子滤波
  16. jQuery实现表单提交验证
  17. rar压缩包加密以及rar密码破解的教程
  18. 芯邦主控的U盘量产教程
  19. 怎么样添加桌面我的计算机,怎么样把我的电脑添加到桌面上
  20. 看完清华最新发布的毕业生去向,我沉默了

热门文章

  1. wgan 不理解 损失函数_[图像盲去噪与GAN]GCBD翻译理解
  2. 局域网即时通讯软件java_如何选择企业即时通讯软件?
  3. 计算机科学技术考研内容,计算机科学技术考研考什么科目
  4. openim php sdk,imsdk_restapi-php-sdk
  5. scala写入mysql_spark rdd转dataframe 写入mysql的实例讲解
  6. by mybatis 自定义order_MyBatis动态SQL实现ORDER BY和LIMIT的控制?
  7. djangosave保存数据太慢_PaddlePaddle从入门到炼丹八——模型的保存与使用
  8. Linux下飞鸽传书项目设计书,Linux 下飞鸽传书设计实现
  9. 突破次元壁障,Python爬虫获取二次元女友
  10. 通过ssh访问git项目