一套代码小程序&Web&Native运行的探索03——处理模板及属性
接上文: 一套代码小程序&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.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: 是不是自闭和标签比如 <br/> 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 //还需要处理<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: 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 //存放解析结束的虚拟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 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 }
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("<!--") == 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 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 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 };
这个时候我们的index代码量便下来了:
1 <!doctype html> 2 <html> 3 <head> 4 <title>起步</title> 5 </head> 6 <body> 7 8 <div id="app"> 9 10 </div> 11 12 <script type="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 = new MVVM({ 26 el: 'app', 27 template: html, 28 data: { 29 name: '叶小钗' 30 } 31 }) 32 33 </script> 34 </body> 35 </html>
我们现在来更改index.js入口文件的代码,这里特别说一下其中的$mount方法,他试试是要做一个这样的事情:
//模板字符串 <div id = "app"> {{message}} </div>
//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 text 152 }) 153 } else { 154 currentParent && currentParent.children.push({ 155 type: 3, 156 text 157 }) 158 } 159 } 160 161 }); 162 163 return root; 164 165 } 166 167 168 //模板解析函数 end 169 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实例拥有唯一id 180 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函数则不需要解析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 function arrToObj(arr) { 245 let map = {}; 246 for(let i = 0, l = arr.length; i < l; i++) { 247 map[arr[i].name] = arr[i].value 248 } 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: 是不是自闭和标签比如 <br/> input 263 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: text 317 }); 318 } else { 319 node.children.push({ 320 type: 3, 321 text: text 322 }); 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 //存放解析结束的虚拟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 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 }
这里仅仅是到输出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: '<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 <ul id='list'> 2 <li class='item'>Item 1</li> 3 <li class='item'>Item 2</li> 4 <li class='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 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 <!doctype html> 2 <html> 3 <head> 4 <title>起步</title> 5 </head> 6 <body> 7 8 <script type="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 name in props) { 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(child instanceof Element) { 31 //递归调用 32 childEl = child.render(); 33 } else { 34 childEl = document.createTextNode(child); 35 } 36 root.append(childEl); 37 } 38 39 this.rootNode = root; 40 return root; 41 } 42 } 43 44 function el(tagName, props, children) { 45 return new Element(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 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}` : '' // data 10 }${ 11 children ? `,${children}` : '' // children 12 })` 13 return code 14 }
事实上这个跟上面那坨代码完成的工作差不多(同样的遍历加递归),只不过他这里还有更多的目的,比如这段代码最终会生成这样的:
_h('div',{},[_h('div',{},["\n "+_s(name)]),_h('input',{}),_h('br',{})])
这段代码会被包装成一个模板类,等待被实例化,显然到这里还没进入我们的模板解析过程,因为里面出现了_s(name),我们如果加一个span的话会变成这样:
1 <div class="c-row search-line" 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"> 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 <div id="app"> 9 10 </div> 11 12 <script type="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 = new MVVM({ 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 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 code 173 //设置属性 等值 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 return code 183 } 184 185 function genChildren(el, checkSkip) { 186 const children = el.children 187 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 checkSkip 196 ? 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 // 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 return data 234 } 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 text 317 }) 318 } else { 319 currentParent && currentParent.children.push({ 320 type: 3, 321 text 322 }) 323 } 324 } 325 326 }); 327 328 //***关键代码*** 329 //将vnode转换为render函数,事实上可以直接传入这种render函数,便不会执行这块逻辑,编译时候会把这块工作做掉 330 return codeGen(root); 331 332 } 333 334 335 //模板解析函数 end 336 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实例拥有唯一id 355 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函数则不需要解析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.render 417 let vnode 418 try { 419 //自动解析的template不需要h,用户自定义的函数需要h 420 vnode = render.call(this, this._h); 421 } catch (e) { 422 warn(`render Error : ${e}`) 423 } 424 return vnode 425 } 426 427 _h(tag, data, children) { 428 return el(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 }
之前我们图简单,一直没有解决属性问题,现在我们在模板里面加入一些属性:
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变成了这个样子:
_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>
生成了如下代码:
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 ./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 虚拟树库,暂时自己写的,后续要换成snabbdom 11 ./index.html 入口文件
今天的学习到此位置,明天我们来处理数据更新相关