接上文: 一套代碼小程序&Web&Native運行的探索02javascript
對應Git代碼地址請見:https://github.com/yexiaochai/wxdemo/tree/master/mvvmhtml
咱們在研究若是小程序在多端運行的時候,基本在前端框架這塊陷入了困境,由於市面上沒有框架能夠直接拿來用,而Vue的相識度比較高,並且口碑很好,咱們便接着這個機會同步學習Vue也解決咱們的問題,咱們看看這個系列結束後,會不會離目標進一點,後續若是實現後會從新整理系列文章......前端
參考:java
https://github.com/fastCreator/MVVM(極度參考,十分感謝該做者,直接看Vue會比較吃力的,可是看完這個做者的代碼便會輕易不少,惋惜這個做者沒有對應博客說明,否則就爽了)node
https://www.tangshuang.net/3756.htmlgit
https://www.cnblogs.com/kidney/p/8018226.htmlgithub
https://github.com/livoras/blog/issues/13express
上文中咱們藉助HTMLParser這種高級神器,終於將文本中的表達式替換了出來,這裏單純說文本這裏也有如下問題:這段是不支持js代碼的,+-、三元代碼都不支持,因此以上都只是幫助咱們理解,仍是以前那句話,越是單純的代碼,越是考慮少的代碼,可能越是能理解實現,可是後續仍然須要補足,咱們這裏仍是要跟Vue對齊,這樣作有個好處,當你不知道怎麼作的時候,能夠看看Vue的實現,當你思考這麼作合不合適的時候,也能夠參考Vue,那但是通過烈火淬鍊的,值得深度學習,咱們今天的任務比較簡單即是完整的處理完style、屬性以及表達式處理,這裏咱們直接在fastCreator這個做者下的源碼開始學習,還有種學習源碼的方法就是抄三次......canvas
咱們學習的過程,先將代碼寫到一塊兒方便理解,後續再慢慢拆分,首先是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 入口文件
今天的學習到此位置,明天咱們來處理數據更新相關