一套程式碼小程式&Web&Native執行的探索03
接上文: ofollow,noindex" target="_blank"> 一套程式碼小程式&Web&Native執行的探索02
我們在研究如果小程式在多端執行的時候,基本在前端框架這塊陷入了困境,因為市面上沒有框架可以直接拿來用,而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) { 4let map = {}; 5for(let i = 0, l = arr.length; i <l; i++) { 6map[arr[i].name] = arr[i].value 7} 8return map; 9 } 10 11 function htmlParser(html) { 12 13//儲存所有節點 14let nodes = []; 15 16//記錄當前節點位置,方便定位parent節點 17let stack = []; 18 19HTMLParser(html, { 20/* 21unary: 是不是自閉和標籤比如 <br/> input 22attrs為屬性的陣列 23*/ 24start: function( tag, attrs, unary ) { //標籤開始 25/* 26stack記錄的父節點,如果節點長度大於1,一定具有父節點 27*/ 28let parent = stack.length ? stack[stack.length - 1] : null; 29 30//最終形成的node物件 31let node = { 32//1標籤, 2需要解析的表示式, 3 純文字 33type: 1, 34tag: tag, 35attrs: arrToObj(attrs), 36parent: parent, 37//關鍵屬性 38children: [] 39}; 40 41//如果存在父節點,也標誌下這個屬於其子節點 42if(parent) { 43parent.children.push(node); 44} 45//還需要處理<br/> <input>這種非閉合標籤 46//... 47 48//進入節點堆疊,當遇到彈出標籤時候彈出 49stack.push(node) 50nodes.push(node); 51 52 //debugger; 53}, 54end: function( tag ) { //標籤結束 55//彈出當前子節點,根節點一定是最後彈出去的,兄弟節點之間會按順序彈出,其父節點在最後一個子節點彈出後會被彈出 56stack.pop(); 57 58 //debugger; 59}, 60chars: function( text ) { //文字 61//如果是空格之類的不予處理 62if(text.trim() === '') return; 63text = text.trim(); 64 65//匹配 {{}} 拿出表示式 66let reg = /\{\{(.*)\}\}/; 67let node = nodes[nodes.length - 1]; 68//如果這裡是表示式{{}}需要特殊處理 69if(!node) return; 70 71if(reg.test(text)) { 72node.children.push({ 73type: 2, 74expression: RegExp.$1, 75text: text 76}); 77} else { 78node.children.push({ 79type: 3, 80text: text 81}); 82} 83 //debugger; 84} 85}); 86 87return nodes; 88 89 } 90 91 export default class MVVM { 92/* 93暫時要求必須傳入data以及el,其他事件什麼的不管 94 95*/ 96constructor(opts) { 97 98//要求必須存在,這裡不做引數校驗了 99this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el; 100 101//data必須存在,其他不做要求 102this.$data = opts.data; 103 104//模板必須存在 105this.$template = opts.template; 106 107//存放解析結束的虛擬dom 108this.$nodes = []; 109 110//將模板解析後,轉換為一個函式 111this.$initRender(); 112 113//渲染之 114this.$render(); 115debugger; 116} 117 118$initRender() { 119let template = this.$template; 120let nodes = htmlParser(template); 121this.$nodes = nodes; 122} 123 124//解析模板生成的函式,將最總html結構渲染出來 125$render() { 126 127let data = this.$data; 128let root = this.$nodes[0]; 129let parent = this._createEl(root); 130//簡單遍歷即可 131 132this._render(parent, root.children); 133 134this.$el.appendChild(parent); 135} 136 137_createEl(node) { 138let data = this.$data; 139 140let el = document.createElement(node.tag || 'span'); 141 142for (let key in node.attrs) { 143el.setAttribute(key, node.attrs[key]) 144} 145 146if(node.type === 2) { 147el.innerText = data[node.expression]; 148} else if(node.type === 3) { 149el.innerText = node.text; 150} 151 152return el; 153} 154_render(parent, children) { 155let child = null; 156for(let i = 0, len = children.length; i < len; i++) { 157child = this._createEl(children[i]); 158parent.append(child); 159if(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*(\/?)>/, 7endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, 8attr = /([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) { 30var obj = {}, items = str.split(","); 31for (var i = 0; i < items.length; i++) 32obj[items[i]] = true; 33return obj; 34 } 35 36 export default function HTMLParser(html, handler) { 37var index, chars, match, stack = [], last = html; 38stack.last = function () { 39return this[this.length - 1]; 40}; 41 42while (html) { 43chars = true; 44 45// Make sure we're not in a script or style element 46if (!stack.last() || !special[stack.last()]) { 47 48// Comment 49if (html.indexOf("<!--") == 0) { 50index = html.indexOf("-->"); 51 52if (index >= 0) { 53if (handler.comment) 54handler.comment(html.substring(4, index)); 55html = html.substring(index + 3); 56chars = false; 57} 58 59// end tag 60} else if (html.indexOf("</") == 0) { 61match = html.match(endTag); 62 63if (match) { 64html = html.substring(match[0].length); 65match[0].replace(endTag, parseEndTag); 66chars = false; 67} 68 69// start tag 70} else if (html.indexOf("<") == 0) { 71match = html.match(startTag); 72 73if (match) { 74html = html.substring(match[0].length); 75match[0].replace(startTag, parseStartTag); 76chars = false; 77} 78} 79 80if (chars) { 81index = html.indexOf("<"); 82 83var text = index < 0 ? html : html.substring(0, index); 84html = index < 0 ? "" : html.substring(index); 85 86if (handler.chars) 87handler.chars(text); 88} 89 90} else { 91html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) { 92text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2"); 93if (handler.chars) 94handler.chars(text); 95 96return ""; 97}); 98 99parseEndTag("", stack.last()); 100} 101 102if (html == last) 103throw "Parse Error: " + html; 104last = html; 105} 106 107// Clean up any remaining tags 108parseEndTag(); 109 110function parseStartTag(tag, tagName, rest, unary) { 111tagName = tagName.toLowerCase(); 112 113if (block[tagName]) { 114while (stack.last() && inline[stack.last()]) { 115parseEndTag("", stack.last()); 116} 117} 118 119if (closeSelf[tagName] && stack.last() == tagName) { 120parseEndTag("", tagName); 121} 122 123unary = empty[tagName] || !!unary; 124 125if (!unary) 126stack.push(tagName); 127 128if (handler.start) { 129var attrs = []; 130 131rest.replace(attr, function (match, name) { 132var value = arguments[2] ? arguments[2] : 133arguments[3] ? arguments[3] : 134arguments[4] ? arguments[4] : 135fillAttrs[name] ? name : ""; 136 137attrs.push({ 138name: name, 139value: value, 140escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" 141}); 142}); 143 144if (handler.start) 145handler.start(tagName, attrs, unary); 146} 147} 148 149function parseEndTag(tag, tagName) { 150// If no tag name is provided, clean shop 151if (!tagName) 152var pos = 0; 153 154// Find the closest opened tag of the same type 155else 156for (var pos = stack.length - 1; pos >= 0; pos--) 157if (stack[pos] == tagName) 158break; 159 160if (pos >= 0) { 161// Close all the open elements, up the stack 162for (var i = stack.length - 1; i >= pos; i--) 163if (handler.end) 164handler.end(stack[i]); 165 166// Remove the open elements from the stack 167stack.length = pos; 168} 169} 170 }; html-parser
這個時候我們的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 14import MVVM from './libs/index.js' 15 16let 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 25let vm = new MVVM({ 26el: 'app', 27template: html, 28data: { 29name: '葉小釵' 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) { 7return typeof obj === 'function' 8 } 9 10 11 function makeAttrsMap(attrs, delimiters) { 12const map = {} 13for (let i = 0, l = attrs.length; i < l; i++) { 14map[attrs[i].name] = attrs[i].value; 15} 16return map; 17 } 18 19 20 21 //dom操作 22 function query(el) { 23if (typeof el === 'string') { 24const selector = el 25el = document.querySelector(el) 26if (!el) { 27return document.createElement('div') 28} 29} 30return el 31 } 32 33 function cached(fn) { 34const cache = Object.create(null) 35return function cachedFn(str) { 36const hit = cache[str] 37return hit || (cache[str] = fn(str)) 38} 39 } 40 41 let idToTemplate = cached(function (id) { 42var el = query(id) 43return 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 => { 56const open = delimiters[0].replace(regexEscapeRE, '\\$&') 57const close = delimiters[1].replace(regexEscapeRE, '\\$&') 58return new RegExp(open + '((?:.|\\n)+?)' + close, 'g') 59}) 60 61 62 function TextParser(text, delimiters) { 63const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE 64if (!tagRE.test(text)) { 65return 66} 67const tokens = [] 68let lastIndex = tagRE.lastIndex = 0 69let match, index 70while ((match = tagRE.exec(text))) { 71index = match.index 72// push text token 73if (index > lastIndex) { 74tokens.push(JSON.stringify(text.slice(lastIndex, index))) 75} 76// tag token 77const exp = match[1].trim() 78tokens.push(`_s(${exp})`) 79lastIndex = index + match[0].length 80} 81if (lastIndex < text.length) { 82tokens.push(JSON.stringify(text.slice(lastIndex))) 83} 84return tokens.join('+') 85 } 86 87 //******核心中的核心 88 function compileToFunctions(template, vm) { 89let root; 90let currentParent; 91let options = vm.$options; 92let stack = []; 93 94//這段程式碼昨天做過解釋,這裡屬性引數比昨天多一些 95HTMLParser(template, { 96start: function(tag, attrs, unary) { 97 98let element = { 99vm: vm, 100//1 標籤 2 文字表示式 3 文字 101type: 1, 102tag, 103//陣列 104attrsList: attrs, 105attrsMap: makeAttrsMap(attrs), //將屬性陣列轉換為物件 106parent: currentParent, 107children: [] 108}; 109 110if(!root) { 111vm.$vnode = root = element; 112} 113 114if(currentParent && !element.forbidden) { 115currentParent.children.push(element); 116element.parent = currentParent; 117} 118 119if(!unary) { 120currentParent = element; 121stack.push(element); 122} 123 124}, 125end: function (tag) { 126//獲取當前元素 127let element = stack[stack.length - 1]; 128let lastNode = element.children[element.children.length - 1]; 129//刪除最後一個空白節點,暫時感覺沒撒用呢 130if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') { 131element.children.pop(); 132} 133 134//據說比呼叫pop節約效能相當於stack.pop() 135stack.length -= 1; 136currentParent = stack[stack.length - 1]; 137 138}, 139//處理真實的節點 140chars: function(text) { 141if (!text.trim()) { 142//text = ' ' 143return; 144} 145//解析文字節點 exp: a{{b}}c => 'a'+_s(a)+'b' 146let expression = TextParser(text, options.delimiters) 147if (expression) { 148currentParent.children.push({ 149type: 2, 150expression, 151text 152}) 153} else { 154currentParent && currentParent.children.push({ 155type: 3, 156text 157}) 158} 159} 160 161}); 162 163return root; 164 165 } 166 167 168 //模板解析函式 end 169 170 //因為我們後面採用setData的方式通知更新,不做響應式更新,這裡也先不考慮update,不考慮監控,先關注首次渲染 171 //要做到更新資料,DOM跟著更新,事實上就是所有的data資料被監控(劫持)起來了,一旦更新都會呼叫對應的回撥,我們這裡做到更新再說 172 function initData(vm, data) { 173if (isFunction(data)) { 174data = data() 175} 176vm.$data = data; 177 } 178 179 //全域性資料保證每個MVVM例項擁有唯一id 180 let uid = 0; 181 182 export default class MVVM { 183constructor(options) { 184this.$options = options; 185 186//我們可以在傳入引數的地方設定標籤替換方式,比如可以設定為['<%=', '%>'],注意這裡是陣列 187this.$options.delimiters = this.$options.delimiters || ["{{", "}}"]; 188 189//唯一標誌 190this._uid = uid++; 191 192if(options.data) { 193// 194initData(this, options.data); 195} 196 197this.$mount(options.el); 198 199} 200 201//解析模板compileToFunctions,將之形成一個函式 202//很多網上的解釋是將例項掛載到dom上,這裡有些沒明白,我們後面點再看看 203$mount(el) { 204let options = this.$options; 205 206el = el && query(el); 207this.$el = el; 208 209//如果使用者自定義了render函式則不需要解析template 210//這裡所謂的使用者自定義,應該是使用者生成了框架生成那坨程式碼,事實上還是將template轉換為vnode 211if(!options.render) { 212lettemplate = options.template; 213if(template) { 214if(typeof template === 'string') { 215//獲取script的template模板 216if (template[0] === '#') { 217template = idToTemplate(template) 218} 219} else if (template.nodeType) { 220//如果template是個dom結構,只能有一個根節點 221template = template.innerHTML; 222} 223} 224 225//上面的程式碼什麼都沒做,只是確保正確的拿到了template資料,考慮了各種情況 226//下面這段是關鍵,也是我們昨天干的事情 227if(template) { 228//***核心函式***/ 229let render = compileToFunctions(template, this); 230options.render = render; 231} 232 233 234} 235 236 237 238} 239 240 241 } 242 243 //過去的程式碼 244 function arrToObj(arr) { 245let map = {}; 246for(let i = 0, l = arr.length; i <l; i++) { 247map[arr[i].name] = arr[i].value 248} 249return map; 250 } 251 252 function htmlParser(html) { 253 254//儲存所有節點 255let nodes = []; 256 257//記錄當前節點位置,方便定位parent節點 258let stack = []; 259 260HTMLParser(html, { 261/* 262unary: 是不是自閉和標籤比如 <br/> input 263attrs為屬性的陣列 264*/ 265start: function( tag, attrs, unary ) { //標籤開始 266/* 267stack記錄的父節點,如果節點長度大於1,一定具有父節點 268*/ 269let parent = stack.length ? stack[stack.length - 1] : null; 270 271//最終形成的node物件 272let node = { 273//1標籤, 2需要解析的表示式, 3 純文字 274type: 1, 275tag: tag, 276attrs: arrToObj(attrs), 277parent: parent, 278//關鍵屬性 279children: [] 280}; 281 282//如果存在父節點,也標誌下這個屬於其子節點 283if(parent) { 284parent.children.push(node); 285} 286//還需要處理<br/> <input>這種非閉合標籤 287//... 288 289//進入節點堆疊,當遇到彈出標籤時候彈出 290stack.push(node) 291nodes.push(node); 292 293 //debugger; 294}, 295end: function( tag ) { //標籤結束 296//彈出當前子節點,根節點一定是最後彈出去的,兄弟節點之間會按順序彈出,其父節點在最後一個子節點彈出後會被彈出 297stack.pop(); 298 299 //debugger; 300}, 301chars: function( text ) { //文字 302//如果是空格之類的不予處理 303if(text.trim() === '') return; 304text = text.trim(); 305 306//匹配 {{}} 拿出表示式 307let reg = /\{\{(.*)\}\}/; 308let node = nodes[nodes.length - 1]; 309//如果這裡是表示式{{}}需要特殊處理 310if(!node) return; 311 312if(reg.test(text)) { 313node.children.push({ 314type: 2, 315expression: RegExp.$1, 316text: text 317}); 318} else { 319node.children.push({ 320type: 3, 321text: text 322}); 323} 324 //debugger; 325} 326}); 327 328return nodes; 329 330 } 331 332 class MVVM1 { 333/* 334暫時要求必須傳入data以及el,其他事件什麼的不管 335 336*/ 337constructor(opts) { 338 339//要求必須存在,這裡不做引數校驗了 340this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el; 341 342//data必須存在,其他不做要求 343this.$data = opts.data; 344 345//模板必須存在 346this.$template = opts.template; 347 348//存放解析結束的虛擬dom 349this.$nodes = []; 350 351//將模板解析後,轉換為一個函式 352this.$initRender(); 353 354//渲染之 355this.$render(); 356debugger; 357} 358 359$initRender() { 360let template = this.$template; 361let nodes = htmlParser(template); 362this.$nodes = nodes; 363} 364 365//解析模板生成的函式,將最總html結構渲染出來 366$render() { 367 368let data = this.$data; 369let root = this.$nodes[0]; 370let parent = this._createEl(root); 371//簡單遍歷即可 372 373this._render(parent, root.children); 374 375this.$el.appendChild(parent); 376} 377 378_createEl(node) { 379let data = this.$data; 380 381let el = document.createElement(node.tag || 'span'); 382 383for (let key in node.attrs) { 384el.setAttribute(key, node.attrs[key]) 385} 386 387if(node.type === 2) { 388el.innerText = data[node.expression]; 389} else if(node.type === 3) { 390el.innerText = node.text; 391} 392 393return el; 394} 395_render(parent, children) { 396let child = null; 397for(let i = 0, len = children.length; i < len; i++) { 398child = this._createEl(children[i]); 399parent.append(child); 400if(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({ 2render: function () { 3return this._h('div', { 4attrs:{ 5a: 'aaa' 6} 7}, [ 8this._h('div') 9]) 10} 11 })
他對應的這段程式碼:
1 new Vue({ 2template: '<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 = { 2tagName: 'ul', // 節點標籤名 3props: { // DOM的屬性,用一個物件儲存鍵值對 4id: 'list' 5}, 6children: [ // 該節點的子節點 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 { 2constructor(tagName, props, children) { 3this.tagName = tagName; 4this.props = props; 5this.children = children; 6} 7 }
上面的dom結構便可以變成這樣了:
1 new Element('ul', {id: 'list'}, [ 2new Element('li', {class: 'item'}, ['Item 1']), 3new Element('li', {class: 'item'}, ['Item 2']), 4new Element('li', {class: 'item'}, ['Item 3']) 5 ])
似乎程式碼有點不好看,於是封裝下例項化操作:
1 class Element { 2constructor(tagName, props, children) { 3this.tagName = tagName; 4this.props = props; 5this.children = children; 6} 7 } 8 9 function el(tagName, props, children){ 10return new Element(tagName, props, children) 11 } 12 13 el('ul', {id: 'list'}, [ 14el('li', {class: 'item'}, ['Item 1']), 15el('li', {class: 'item'}, ['Item 2']), 16el('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 10class Element { 11constructor(tagName, props, children) { 12this.tagName = tagName; 13this.props = props; 14this.children = children; 15} 16render() { 17//拿著根節點往下面擼 18let root = document.createElement(this.tagName); 19let props = this.props; 20 21for(let name in props) { 22root.setAttribute(name, props[name]); 23} 24 25let children = this.children; 26 27for(let i = 0, l = children.length; i < l; i++) { 28let child = children[i]; 29let childEl; 30if(child instanceof Element) { 31//遞迴呼叫 32childEl = child.render(); 33} else { 34childEl = document.createTextNode(child); 35} 36root.append(childEl); 37} 38 39this.rootNode = root; 40return root; 41} 42} 43 44function el(tagName, props, children){ 45return new Element(tagName, props, children) 46} 47 48let vnode = el('ul', {id: 'list'}, [ 49el('li', {class: 'item'}, ['Item 1']), 50el('li', {class: 'item'}, ['Item 2']), 51el('li', {class: 'item'}, ['Item 3']) 52]) 53 54let root = vnode.render(); 55 56document.body.appendChild(root); 57 58 </script> 59 60 </body> 61 </html>
饒了這麼大一圈子,我們再回頭看這段程式碼:
1 new Vue({ 2render: function () { 3return this._h('div', { 4attrs:{ 5a: 'aaa' 6} 7}, [ 8this._h('div') 9]) 10} 11 })
這個時候,我們對這個_h幹了什麼,可能便有比較清晰的認識了,於是我們回到我們之前的程式碼,暫時跳出snabbdom
解析模板
在render中,我們有這麼一段程式碼:
1 //沒有指令時執行,或者指令解析完畢 2 function nodir(el) { 3let code 4//設定屬性 等值 5const data = genData(el); 6//轉換子節點 7const children = genChildren(el, true); 8code = `_h('${el.tag}'${ 9data ? `,${data}` : '' // data 10 }${ 11children ? `,${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 14import MVVM from './libs/index.js' 15 16let 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 26let vm = new MVVM({ 27el: '#app', 28template: html, 29data: { 30name: '葉小釵', 31age: 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) { 7return typeof obj === 'function' 8 } 9 10 11 function makeAttrsMap(attrs, delimiters) { 12const map = {} 13for (let i = 0, l = attrs.length; i < l; i++) { 14map[attrs[i].name] = attrs[i].value; 15} 16return map; 17 } 18 19 20 21 //dom操作 22 function query(el) { 23if (typeof el === 'string') { 24const selector = el 25el = document.querySelector(el) 26if (!el) { 27return document.createElement('div') 28} 29} 30return el 31 } 32 33 function cached(fn) { 34const cache = Object.create(null) 35return function cachedFn(str) { 36const hit = cache[str] 37return hit || (cache[str] = fn(str)) 38} 39 } 40 41 let idToTemplate = cached(function (id) { 42var el = query(id) 43return 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 => { 56const open = delimiters[0].replace(regexEscapeRE, '\\$&') 57const close = delimiters[1].replace(regexEscapeRE, '\\$&') 58return new RegExp(open + '((?:.|\\n)+?)' + close, 'g') 59}) 60 61 62 function TextParser(text, delimiters) { 63const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE 64if (!tagRE.test(text)) { 65return 66} 67const tokens = [] 68let lastIndex = tagRE.lastIndex = 0 69let match, index 70while ((match = tagRE.exec(text))) { 71index = match.index 72// push text token 73if (index > lastIndex) { 74tokens.push(JSON.stringify(text.slice(lastIndex, index))) 75} 76// tag token 77const exp = match[1].trim() 78tokens.push(`_s(${exp})`) 79lastIndex = index + match[0].length 80} 81if (lastIndex < text.length) { 82tokens.push(JSON.stringify(text.slice(lastIndex))) 83} 84return tokens.join('+') 85 } 86 87 function makeFunction(code) { 88try { 89return new Function(code) 90} catch (e) { 91return function (){}; 92} 93 } 94 95 //***虛擬dom部分程式碼,後續會換成snabdom 96 class Element { 97constructor(tagName, props, children) { 98this.tagName = tagName; 99this.props = props; 100this.children = children || []; 101} 102render() { 103//拿著根節點往下面擼 104let el = document.createElement(this.tagName); 105let props = this.props; 106 107for(let name in props) { 108el.setAttribute(name, props[name]); 109} 110 111let children = this.children; 112 113for(let i = 0, l = children.length; i < l; i++) { 114let child = children[i]; 115let childEl; 116if(child instanceof Element) { 117//遞迴呼叫 118childEl = child.render(); 119} else { 120childEl = document.createTextNode(child); 121} 122el.append(childEl); 123} 124return el; 125} 126 } 127 128 function el(tagName, props, children){ 129return 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 = { 136stop: '$event.stopPropagation();', 137prevent: '$event.preventDefault();', 138self: 'if($event.target !== $event.currentTarget)return;', 139ctrl: 'if(!$event.ctrlKey)return;', 140shift: 'if(!$event.shiftKey)return;', 141alt: 'if(!$event.altKey)return;', 142meta: 'if(!$event.metaKey)return;' 143 } 144 145 const keyCodes = { 146esc: 27, 147tab: 9, 148enter: 13, 149space: 32, 150up: 38, 151left: 37, 152right: 39, 153down: 40, 154'delete': [8, 46] 155 } 156 157 158 function codeGen(ast) { 159//解析成h render字串形式 160const code = ast ? genElement(ast) : '_h("div")' 161//把render函式,包起來,使其在當前作用域內 162return makeFunction(`with(this){ debugger; return ${code}}`) 163 } 164 165 function genElement(el) { 166//無指令 167return nodir(el) 168 } 169 170 //沒有指令時執行,或者指令解析完畢 171 function nodir(el) { 172let code 173//設定屬性 等值 174const data = genData(el); 175//轉換子節點 176const children = genChildren(el, true); 177code = `_h('${el.tag}'${ 178data ? `,${data}` : '' // data 179 }${ 180children ? `,${children}` : '' // children 181 })` 182 return code 183 } 184 185 function genChildren(el, checkSkip) { 186const children = el.children 187if (children.length) { 188const el = children[0] 189// 如果是v-for 190//if (children.length === 1 && el.for) { 191//return genElement(el) 192//} 193const normalizationType = 0 194return `[${children.map(genNode).join(',')}]${ 195checkSkip 196? normalizationType ? `,${normalizationType}` : '' 197: '' 198}` 199 } 200 } 201 202 function genNode(node) { 203if (node.type === 1) { 204return genElement(node) 205} else { 206return genText(node) 207} 208 } 209 210 function genText(text) { 211return text.type === 2 ? text.expression : JSON.stringify(text.text) 212 } 213 214 function genData(el) { 215let data = '{' 216// attributes 217if (el.style) { 218data += 'style:' + genProps(el.style) + ',' 219} 220if (Object.keys(el.attrs).length) { 221data += 'attrs:' + genProps(el.attrs) + ',' 222} 223if (Object.keys(el.props).length) { 224data += 'props:' + genProps(el.props) + ',' 225} 226if (Object.keys(el.events).length) { 227data += 'on:' + genProps(el.events) + ',' 228} 229if (Object.keys(el.hook).length) { 230data += 'hook:' + genProps(el.hook) + ',' 231} 232data = data.replace(/,$/, '') + '}' 233return data 234 } 235 236 function genProps(props) { 237let res = '{'; 238for (let key in props) { 239res += `"${key}":${props[key]},` 240} 241return res.slice(0, -1) + '}' 242 } 243 244 //******核心中的核心 245 function compileToFunctions(template, vm) { 246let root; 247let currentParent; 248let options = vm.$options; 249let stack = []; 250 251//這段程式碼昨天做過解釋,這裡屬性引數比昨天多一些 252HTMLParser(template, { 253start: function(tag, attrs, unary) { 254 255let element = { 256vm: vm, 257//1 標籤 2 文字表示式 3 文字 258type: 1, 259tag, 260//陣列 261attrsList: attrs, 262attrsMap: makeAttrsMap(attrs), //將屬性陣列轉換為物件 263parent: currentParent, 264children: [], 265 266//下面這些屬性先不予關注,因為底層函式沒有做校驗,不傳要報錯 267events: {}, 268style: null, 269hook: {}, 270props: {},//DOM屬性 271attrs: {}//值為true,false則移除該屬性 272 273}; 274 275if(!root) { 276vm.$vnode = root = element; 277} 278 279if(currentParent && !element.forbidden) { 280currentParent.children.push(element); 281element.parent = currentParent; 282} 283 284if(!unary) { 285currentParent = element; 286stack.push(element); 287} 288 289}, 290end: function (tag) { 291//獲取當前元素 292let element = stack[stack.length - 1]; 293let lastNode = element.children[element.children.length - 1]; 294//刪除最後一個空白節點,暫時感覺沒撒用呢 295if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') { 296element.children.pop(); 297} 298 299//據說比呼叫pop節約效能相當於stack.pop() 300stack.length -= 1; 301currentParent = stack[stack.length - 1]; 302 303}, 304//處理真實的節點 305chars: function(text) { 306if (!text.trim()) { 307//text = ' ' 308return; 309} 310//解析文字節點 exp: a{{b}}c => 'a'+_s(a)+'b' 311let expression = TextParser(text, options.delimiters) 312if (expression) { 313currentParent.children.push({ 314type: 2, 315expression, 316text 317}) 318} else { 319currentParent && currentParent.children.push({ 320type: 3, 321text 322}) 323} 324} 325 326}); 327 328//***關鍵程式碼*** 329//將vnode轉換為render函式,事實上可以直接傳入這種render函式,便不會執行這塊邏輯,編譯時候會把這塊工作做掉 330return codeGen(root); 331 332 } 333 334 335 //模板解析函式 end 336 337 //因為我們後面採用setData的方式通知更新,不做響應式更新,這裡也先不考慮update,不考慮監控,先關注首次渲染 338 //要做到更新資料,DOM跟著更新,事實上就是所有的data資料被監控(劫持)起來了,一旦更新都會呼叫對應的回撥,我們這裡做到更新再說 339 function initData(vm, data) { 340if (isFunction(data)) { 341data = data() 342} 343 344//這裡將data上的資料移植到this上,後面要監控 345for(let key in data) { 346 347//這裡有可能會把自身方法覆蓋,所以自身的屬性方法需要+$ 348vm[key] = data[key]; 349} 350 351vm.$data = data; 352 } 353 354 //全域性資料保證每個MVVM例項擁有唯一id 355 let uid = 0; 356 357 export default class MVVM { 358constructor(options) { 359this.$options = options; 360 361//我們可以在傳入引數的地方設定標籤替換方式,比如可以設定為['<%=', '%>'],注意這裡是陣列 362this.$options.delimiters = this.$options.delimiters || ["{{", "}}"]; 363 364//唯一標誌 365this._uid = uid++; 366 367if(options.data) { 368// 369initData(this, options.data); 370} 371 372this.$mount(options.el); 373 374let _node = this._render().render(); 375this.$el.appendChild( _node) 376 377} 378 379//解析模板compileToFunctions,將之形成一個函式 380//很多網上的解釋是將例項掛載到dom上,這裡有些沒明白,我們後面點再看看 381$mount(el) { 382let options = this.$options; 383 384el = el && query(el); 385this.$el = el; 386 387//如果使用者自定義了render函式則不需要解析template 388//這裡所謂的使用者自定義,應該是使用者生成了框架生成那坨程式碼,事實上還是將template轉換為vnode 389if(!options.render) { 390lettemplate = options.template; 391if(template) { 392if(typeof template === 'string') { 393//獲取script的template模板 394if (template[0] === '#') { 395template = idToTemplate(template) 396} 397} else if (template.nodeType) { 398//如果template是個dom結構,只能有一個根節點 399template = template.innerHTML; 400} 401} 402 403//上面的程式碼什麼都沒做,只是確保正確的拿到了template資料,考慮了各種情況 404//下面這段是關鍵,也是我們昨天干的事情 405if(template) { 406//***核心函式***/ 407let render = compileToFunctions(template, this); 408options.render = render; 409} 410} 411 412return this; 413} 414 415_render() { 416let render = this.$options.render 417let vnode 418try { 419//自動解析的template不需要h,使用者自定義的函式需要h 420vnode = render.call(this, this._h); 421} catch (e) { 422warn(`render Error : ${e}`) 423} 424return vnode 425} 426 427_h(tag, data, children) { 428return el(tag, data, children) 429} 430 431_s(val) { 432return val == null 433? '' 434: typeof val === 'object' 435? JSON.stringify(val, null, 2) 436: String(val) 437} 438 439 } libs/index.js
今天的學習就到這裡,我們下次將屬性和class解析出來就可以了