博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一套代码小程序&Web&Native运行的探索03——处理模板及属性
阅读量:734 次
发布时间:2019-03-22

本文共 35548 字,大约阅读时间需要 118 分钟。

接上文:

对应Git代码地址请见:

我们在研究如果小程序在多端运行的时候,基本在前端框架这块陷入了困境,因为市面上没有框架可以直接拿来用,而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.js index.html
1 import HTMLParser from './html-parser.js'  2   3 function arrToObj(arr) {  4   let map = {};  5   for(let i = 0, l = arr.length; i <  l; i++) {  6     map[arr[i].name] = arr[i].value  7   }  8   return map;  9 } 10  11 function htmlParser(html) { 12  13   //存储所有节点 14   let nodes = []; 15  16   //记录当前节点位置,方便定位parent节点 17   let stack = []; 18  19   HTMLParser(html, { 20     /* 21      unary: 是不是自闭和标签比如 
input 22 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 //还需要处理
这种非闭合标签 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: text 76 }); 77 } else { 78 node.children.push({ 79 type: 3, 80 text: text 81 }); 82 } 83 // debugger; 84 } 85 }); 86 87 return nodes; 88 89 } 90 91 export default class 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 //存放解析结束的虚拟dom108 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 in node.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 return el;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-Parser  3  */  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]+)))?/g  9  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 function makeMap(str) { 30     var obj = {}, items = str.split(","); 31     for (var i = 0; i < items.length; i++) 32         obj[items[i]] = true; 33     return obj; 34 } 35  36 export default function HTMLParser(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("
"); 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("
]*>"), function (all, text) { 92 text = text.replace(/
|
/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 tags108 parseEndTag();109 110 function parseStartTag(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 function parseEndTag(tag, tagName) {150 // If no tag name is provided, clean shop151 if (!tagName)152 var pos = 0;153 154 // Find the closest opened tag of the same type155 else156 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 stack162 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 stack167 stack.length = pos;168 }169 }170 };
html-parser

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

1  2  3  4   起步 5  6  7  8 
9 10
11 12 34 35

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

//模板字符串
{{message}}
//render函数function anonymous() {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 function isFunction(obj) {  7   return typeof obj === 'function'  8 }  9  10  11 function makeAttrsMap(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   return map; 17 } 18  19  20  21 //dom操作 22 function query(el) { 23   if (typeof el === 'string') { 24     const selector = el 25     el = document.querySelector(el) 26     if (!el) { 27       return document.createElement('div') 28     } 29   } 30   return el 31 } 32  33 function cached(fn) { 34   const cache = Object.create(null) 35   return function cachedFn(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)+?)\}\}/g 53 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 function TextParser(text, delimiters) { 63   const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE 64   if (!tagRE.test(text)) { 65     return 66   } 67   const tokens = [] 68   let lastIndex = tagRE.lastIndex = 0 69   let match, index 70   while ((match = tagRE.exec(text))) { 71     index = match.index 72     // 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].length 80   } 81   if (lastIndex < text.length) { 82     tokens.push(JSON.stringify(text.slice(lastIndex))) 83   } 84   return tokens.join('+') 85 } 86  87 //******核心中的核心 88 function compileToFunctions(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   return root;164 165 }166 167 168 //模板解析函数 end169 170 //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染171 //要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说172 function initData(vm, data) {173   if (isFunction(data)) {174     data = data()175   }176   vm.$data = data;177 }178 179 //全局数据保证每个MVVM实例拥有唯一id180 let uid = 0;181 182 export default class 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函数则不需要解析template210     //这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode211     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 function arrToObj(arr) {245   let map = {};246   for(let i = 0, l = arr.length; i <  l; i++) {247     map[arr[i].name] = arr[i].value248   }249   return map;250 }251 252 function htmlParser(html) {253 254   //存储所有节点255   let nodes = [];256 257   //记录当前节点位置,方便定位parent节点258   let stack = [];259 260   HTMLParser(html, {261     /*262      unary: 是不是自闭和标签比如 
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 //还需要处理
这种非闭合标签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 return nodes;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 //存放解析结束的虚拟dom349 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 in node.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 return el;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 new Vue({ 2     render: function () { 3         return this._h('div', { 4             attrs:{ 5                 a: 'aaa' 6             } 7         }, [ 8            this._h('div') 9         ])10     }11 })

他对应的这段代码:

1 new Vue({2     template: '
Hello World!
'3 })

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

snabbdom-render

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

我们一段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 
    2
  • Item 1
  • 3
  • Item 2
  • 4
  • Item 3
  • 5

同样的,我们一段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 function el(tagName, props, children)  {10   return new Element(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  2  3  4   起步 5  6  7  8 59 60 61 

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

1 new Vue({ 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 function nodir(el) { 3   let code 4   //设置属性 等值 5   const data = genData(el); 6   //转换子节点 7   const children = genChildren(el, true); 8   code = `_h('${el.tag}'${ 9         data ? `,${data}` : '' // data10 }${11         children ? `,${children}` : '' // children12 })`13 return code14 }

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

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

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

1 
2
3 {{name}}
4
{{age+1}}5
6
7
_h('div',{},[_h('div',{},["\n    "+_s(name)]),_h('span',{},[_s(age+1)]),_h('input',{}),_h('br',{})])

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

 

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

1  2  3  4   起步 5  6  7  8 
9 10
11 12 36 37
1 import HTMLParser from './html-parser.js'  2   3   4 //工具函数 begin  5   6 function isFunction(obj) {  7   return typeof obj === 'function'  8 }  9  10  11 function makeAttrsMap(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   return map; 17 } 18  19  20  21 //dom操作 22 function query(el) { 23   if (typeof el === 'string') { 24     const selector = el 25     el = document.querySelector(el) 26     if (!el) { 27       return document.createElement('div') 28     } 29   } 30   return el 31 } 32  33 function cached(fn) { 34   const cache = Object.create(null) 35   return function cachedFn(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)+?)\}\}/g 53 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 function TextParser(text, delimiters) { 63   const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE 64   if (!tagRE.test(text)) { 65     return 66   } 67   const tokens = [] 68   let lastIndex = tagRE.lastIndex = 0 69   let match, index 70   while ((match = tagRE.exec(text))) { 71     index = match.index 72     // 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].length 80   } 81   if (lastIndex < text.length) { 82     tokens.push(JSON.stringify(text.slice(lastIndex))) 83   } 84   return tokens.join('+') 85 } 86  87 function makeFunction(code) { 88   try { 89     return new Function(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 in props) {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 instanceof Element) {117         //递归调用118         childEl = child.render();119       } else {120         childEl = document.createTextNode(child);121       }122       el.append(childEl);123     }124     return el;125   }126 }127 128 function el(tagName, props, children)  {129   return new Element(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 function codeGen(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 function genElement(el) {166   //无指令167   return nodir(el)168 }169 170 //没有指令时运行,或者指令解析完毕171 function nodir(el) {172   let code173   //设置属性 等值174   const data = genData(el);175   //转换子节点176   const children = genChildren(el, true);177   code = `_h('${el.tag}'${178         data ? `,${data}` : '' // data179 }${180         children ? `,${children}` : '' // children181 })`182 return code183 }184 185 function genChildren(el, checkSkip) {186   const children = el.children187   if (children.length) {188     const el = children[0]189     // 如果是v-for190     //if (children.length === 1 && el.for) {191     //  return genElement(el)192     //}193     const normalizationType = 0194     return `[${children.map(genNode).join(',')}]${195             checkSkip196                 ? normalizationType ? `,${normalizationType}` : ''197   : ''198   }`199 }200 }201 202 function genNode(node) {203   if (node.type === 1) {204     return genElement(node)205   } else {206     return genText(node)207   }208 }209 210 function genText(text) {211   return text.type === 2 ? text.expression : JSON.stringify(text.text)212 }213 214 function genData(el) {215   let data = '{'216   // attributes217   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   return data234 }235 236 function genProps(props) {237   let res = '{';238   for (let key in props) {239     res += `"${key}":${props[key]},`240   }241   return res.slice(0, -1) + '}'242 }243 244 //******核心中的核心245 function compileToFunctions(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   return codeGen(root);331 332 }333 334 335 //模板解析函数 end336 337 //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染338 //要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说339 function initData(vm, data) {340   if (isFunction(data)) {341     data = data()342   }343 344   //这里将data上的数据移植到this上,后面要监控345   for(let key in data) {346 347     //这里有可能会把自身方法覆盖,所以自身的属性方法需要+$348     vm[key] = data[key];349   }350 351   vm.$data = data;352 }353 354 //全局数据保证每个MVVM实例拥有唯一id355 let uid = 0;356 357 export default class 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函数则不需要解析template388     //这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode389     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,用户自定义的函数需要h420       vnode = render.call(this, this._h);421     } catch (e) {422       warn(`render Error : ${e}`)423     }424     return vnode425   }426 427   _h(tag, data, children) {428     return el(tag, data, children)429   }430 431   _s(val) {432     return val == null433       ? ''434       : typeof val === 'object'435       ? JSON.stringify(val, null, 2)436       : String(val)437   }438 439 }
libs/index.js

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

1 
2
3 {{name}}
4
{{age+1}}5
6
7

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

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 
2
3
4 叶小钗
5
316
7
8
9

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

{{name}}
{{age+1}}
libs/index.js

生成了如下代码:

1 
2
3 叶小钗
4
315
6
7

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

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

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

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

文件分拆

文件拆分后代码顺便传到了github上:

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

1 ./libs 2 ..../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 虚拟树库,暂时自己写的,后续要换成snabbdom11 ./index.html 入口文件

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

你可能感兴趣的文章