前面咱們對微信小程序進行了研究:【微信小程序項目實踐總結】30分鐘從陌生到熟悉javascript
在實際代碼過程當中咱們發現,咱們可能又要作H5站又要作小程序同時還要作個APP,這裏會形成很大的資源浪費,若是設定一個規則,讓咱們能夠先寫H5代碼,而後將小程序以及APP的業務差別代碼作掉,豈不快哉?但小程序的web框架並不開源,否則也用不着咱們在此費力了,通過研究,小程序web端框架是一套自研的MVVM框架,因而咱們立刻想到了藉助第三方框架:html
通過簡單的研究,咱們發現不管React或者Vue都是能夠必定程度適應小程序開發模式的,市面上也有了對應的框架:mpvue&wepyjava
在使用以上任何一套框架體系前,咱們都須要對MVVM框架有最基礎的瞭解,否則後面隨便遇到點什麼問題可能都會變得難以繼續,負責人要對團隊負責而不是單純只是想技術嚐鮮,因此生產項目必定要使用本身能完成hold住的技術node
這段時間咱們嘗試着去閱讀Vue的源碼,但如今的Vue是一個工程化產物,咱們要學習的核心代碼可能不到其中的1/3,多出來的是各類特性以及細節處理,在不清楚代碼意圖(目標)的狀況下,事實上很難搞懂這段代碼是要幹什麼,不少代碼的出現都是一些精妙的小優化,知道的就會以爲十分驚豔,不知道的就會一頭霧水,一來就看Vue的源碼反而不利於深刻了解webpack
出於這個緣由在網上看了不少源碼介紹的文章,出來就放一個Vue官方的流程圖,而後一套組合模塊套路,基本就把我給打暈了,查詢了不少資料,仍是發現一些寫的比較清晰的(我感受適合多數人的)文章:git
讀懂源碼:一步一步實現一個 Vuegithub
https://github.com/fastCreator/MVVM(特別推薦,很是不錯)web
這兩篇文章都有一個主旨:canvas
在沒有相關框架經驗的狀況下,單單靠單步調試以及網上的源碼介紹,想要讀懂Vue源碼是不太靠譜的作法,比較好的作法是本身照着Vue的源碼寫一套簡單的,最基礎的MVVM框架,在完成這個框架後再去閱讀Vue或者React的代碼要輕易的多,我這邊是很是承認這個說法的,因此咱們照着fastCreateor的代碼(他應該是參考的Vue)也擼了一個:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
這裏與其說擼了一個MVVM框架,不如說給fastCreateor的代碼加上了自我理解的註釋,經過這個過程也對MVVM框架有了第一步的認識,以前的文章或者代碼都有些散,這裏將前面學習的內容再作一次彙總,多加一些圖示,幫助本身也幫助讀者更好的瞭解,因而讓咱們開始吧!
PS:下面說的MVVM框架,基本就是Vue框架,而且是本身的理解,有問題請你們拍磚
咱們梳理了MVVM框架的基本流程,這裏只看首次渲染的話:
① 解析html模板造成mvvm實例對象element(實例上的$node屬性)
② 處理element屬性,這裏包括屬性處理、事件處理、指令處理
③ 使用處理過的element對象,爲每一個實例建立render方法
PS:new MVVM只會產生一個實例,每一個html標籤都會造成一個vnode,組件會造成獨立的實例,與根實例以$parent與$children維護關係
④ 使用render方法建立虛擬dom vnode,vm實例element已經具有全部建立虛擬dom的必要條件,render只是利用他們,若是代碼組織得好,不使用render也行
⑤ render執行後會生成虛擬dom vnode,藉助另外一個神器snabbdom開始對比新舊虛擬dom的結構,完成最終渲染
PS:render執行時做用域在mvvm實例(vm)下
因此整個代碼核心所有是圍繞着HTML=>element($node中間項,橋樑)=>render函數(執行返回vnode)=>引用snabbdom patch渲染
而抓住幾個點後,對應的幾個核心技術點也就出來了:
① 模板解析這裏對應着 HTMLParser,幫忙解決了不少問題
② 造成vnode須要的render函數,而且調用後維護彼此關係,這個是框架作的最多的工做
③ 生成真正的vnode,而後執行對比差別渲染patch操做,這塊重要的工做由snabbdom接手了
咱們再把這裏的目標映射成過程,就獲得了這張圖了(來自https://github.com/fastCreator/MVVM):
上面一行就是首次渲染執行的流程,下面幾個圖就是實現數據變化時候更新試圖的操做,分解到程序層面,核心就是:
① 實例化
② Parser => HTMLParser
③ codegen
在此基礎上再包裝出數據響應模型以及組件系統、指令系統,每一個模塊都很獨立,但又互相關聯,抓住這個主幹看各個分支這樣就會相對比較清晰。因此網上不少幾百行代碼實現MVVM框架核心的就是隻作最核心這一塊,好比這個學習材料:https://github.com/DMQ/mvvm,很是簡單清晰,爲了幫助更好的理解,咱們這裏也寫了一段比較獨立的代碼,包括了核心流程:
1 <div id="app"> 2 <input type="text" v-model="name"> 3 {{name}} 4 </div> 5 6 <script type="text/javascript" > 7 8 function getElById(id) { 9 return document.getElementById(id); 10 } 11 12 //主體對象,存儲全部的訂閱者 13 function Dep () { 14 this.subs = []; 15 } 16 17 //通知全部訂閱者數據變化 18 Dep.prototype.notify = function () { 19 for(let i = 0, l = this.subs.length; i < l; i++) { 20 this.subs[i].update(); 21 } 22 } 23 24 //添加訂閱者 25 Dep.prototype.addSub = function (sub) { 26 this.subs.push(sub); 27 } 28 29 let globalDataDep = new Dep(); 30 31 //觀察者,框架會接觸data的每個與node相關的屬性, 32 //若是data沒有與任何節點產生關聯,則不予理睬 33 //實際的訂閱者對象 34 //注意,只要一個數據對象對應了一個node對象就會生成一個訂閱者,因此真實通知的時候應該須要作到通知到對應數據的dom,這裏不予關注 35 function Watcher(vm, node, name) { 36 this.name = name; 37 this.node = node; 38 this.vm = vm; 39 if(node.nodeType === 1) { 40 this.node.value = this.vm.data[name]; 41 } else if(node.nodeType === 3) { 42 this.node.nodeValue = this.vm.data[name] || ''; 43 } 44 globalDataDep.addSub(this); 45 46 } 47 48 Watcher.prototype.update = function () { 49 if(this.node.nodeType === 1) { 50 this.node.value = this.vm.data[this.name ]; 51 } else if(this.node.nodeType === 3) { 52 this.node.nodeValue = this.vm.data[this.name ] || ''; 53 } 54 } 55 56 //這塊代碼僅作功能說明,不用當真 57 function compile(node, vm) { 58 let reg = /\{\{(.*)\}\}/; 59 60 //節點類型 61 if(node.nodeType === 1) { 62 let attrs = node.attributes; 63 //解析屬性 64 for(let i = 0, l = attrs.length; i < l; i++) { 65 if(attrs[i].nodeName === 'v-model') { 66 let name = attrs[i].nodeValue; 67 if(node.value === vm.data[name]) break; 68 69 // node.value = vm.data[name] || ''; 70 new Watcher(vm, node, name) 71 72 //此處不作太多判斷,直接綁定事件 73 node.addEventListener('input', function (e) { 74 //賦值操做 75 let newObj = {}; 76 newObj[name] = e.target.value; 77 vm.setData(newObj, true); 78 }); 79 80 break; 81 } 82 } 83 } else if(node.nodeType === 3) { 84 85 if(reg.test(node.nodeValue)) { 86 let name = RegExp.$1; // 獲取匹配到的name 87 name = name.trim(); 88 // node.nodeValue = vm.data[name] || ''; 89 new Watcher(vm, node, name) 90 } 91 } 92 } 93 94 //獲取節點 95 function nodeToFragment(node, vm) { 96 let flag = document.createDocumentFragment(); 97 let child; 98 99 while (child = node.firstChild) { 100 compile(child, vm); 101 flag.appendChild(child); 102 } 103 104 return flag; 105 } 106 107 function MVVM(options) { 108 this.data = options.data; 109 let el = getElById(options.el); 110 this.$dom = nodeToFragment(el, this) 111 this.$el = el.appendChild(this.$dom); 112 113 // this.$bindEvent(); 114 } 115 116 MVVM.prototype.setData = function (data, noNotify) { 117 for(let k in data) { 118 this.data[k] = data[k]; 119 } 120 //執行更新邏輯 121 // if(noNotify) return; 122 globalDataDep.notify(); 123 } 124 125 let mvvm = new MVVM({ 126 el: 'app', 127 data: { 128 name: '葉小釵' 129 } 130 }) 131 132 setTimeout(function() { 133 mvvm.setData({name: '刀狂劍癡葉小釵'}) 134 }, 3000) 135 136 </script>
你們對照着這個例子本身擼一下,其中有幾個在業務中不太經常使用的知識點,第一個就是訪問器屬性,這裏大概寫個例子介紹下:
var obj = { }; // 爲obj定義一個名爲 name 的訪問器屬性 Object.defineProperty(obj, "name", { get: function () { console.log('get', arguments); }, set: function (val) { console.log('set', arguments); } }) obj.name = '葉小釵' console.log(obj, obj.name) /* set Arguments ["葉小釵", callee: ƒ, Symbol(Symbol.iterator): ƒ] get Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ] */
接下來咱們對MVVM框架會用到的兩大神器依次作下介紹
HTMLParser這個庫的代碼在這裏能夠拿到:https://github.com/yexiaochai/wxdemo/blob/master/mvvm/libs/html-parser.js
這個庫完成的功能比較簡單,就是解析你傳入的html模板,這裏舉個例子:
1 <!doctype html> 2 <html> 3 <head> 4 <title>起步</title> 5 </head> 6 <body> 7 8 <div id="app"> 9 10 </div> 11 <script > 12 13 // Regular Expressions for parsing tags and attributes 14 let startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, 15 endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, 16 attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g 17 18 // Empty Elements - HTML 5 19 let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr") 20 21 // Block Elements - HTML 5 22 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") 23 24 // Inline Elements - HTML 5 25 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") 26 27 // Elements that you can, intentionally, leave open 28 // (and which close themselves) 29 let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr") 30 31 // Attributes that have their values filled in disabled="disabled" 32 let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected") 33 34 // Special Elements (can contain anything) 35 let special = makeMap("script,style") 36 37 function makeMap(str) { 38 var obj = {}, items = str.split(","); 39 for (var i = 0; i < items.length; i++) 40 obj[items[i]] = true; 41 return obj; 42 } 43 44 function HTMLParser(html, handler) { 45 var index, chars, match, stack = [], last = html; 46 stack.last = function () { 47 return this[this.length - 1]; 48 }; 49 50 while (html) { 51 chars = true; 52 53 // Make sure we're not in a script or style element 54 if (!stack.last() || !special[stack.last()]) { 55 56 // Comment 57 if (html.indexOf("<!--") == 0) { 58 index = html.indexOf("-->"); 59 60 if (index >= 0) { 61 if (handler.comment) 62 handler.comment(html.substring(4, index)); 63 html = html.substring(index + 3); 64 chars = false; 65 } 66 67 // end tag 68 } else if (html.indexOf("</") == 0) { 69 match = html.match(endTag); 70 71 if (match) { 72 html = html.substring(match[0].length); 73 match[0].replace(endTag, parseEndTag); 74 chars = false; 75 } 76 77 // start tag 78 } else if (html.indexOf("<") == 0) { 79 match = html.match(startTag); 80 81 if (match) { 82 html = html.substring(match[0].length); 83 match[0].replace(startTag, parseStartTag); 84 chars = false; 85 } 86 } 87 88 if (chars) { 89 index = html.indexOf("<"); 90 91 var text = index < 0 ? html : html.substring(0, index); 92 html = index < 0 ? "" : html.substring(index); 93 94 if (handler.chars) 95 handler.chars(text); 96 } 97 98 } else { 99 html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) { 100 text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2"); 101 if (handler.chars) 102 handler.chars(text); 103 104 return ""; 105 }); 106 107 parseEndTag("", stack.last()); 108 } 109 110 if (html == last) 111 throw "Parse Error: " + html; 112 last = html; 113 } 114 115 // Clean up any remaining tags 116 parseEndTag(); 117 118 function parseStartTag(tag, tagName, rest, unary) { 119 tagName = tagName.toLowerCase(); 120 121 if (block[tagName]) { 122 while (stack.last() && inline[stack.last()]) { 123 parseEndTag("", stack.last()); 124 } 125 } 126 127 if (closeSelf[tagName] && stack.last() == tagName) { 128 parseEndTag("", tagName); 129 } 130 131 unary = empty[tagName] || !!unary; 132 133 if (!unary) 134 stack.push(tagName); 135 136 if (handler.start) { 137 var attrs = []; 138 139 rest.replace(attr, function (match, name) { 140 var value = arguments[2] ? arguments[2] : 141 arguments[3] ? arguments[3] : 142 arguments[4] ? arguments[4] : 143 fillAttrs[name] ? name : ""; 144 145 attrs.push({ 146 name: name, 147 value: value, 148 escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" 149 }); 150 }); 151 152 if (handler.start) 153 handler.start(tagName, attrs, unary); 154 } 155 } 156 157 function parseEndTag(tag, tagName) { 158 // If no tag name is provided, clean shop 159 if (!tagName) 160 var pos = 0; 161 162 // Find the closest opened tag of the same type 163 else 164 for (var pos = stack.length - 1; pos >= 0; pos--) 165 if (stack[pos] == tagName) 166 break; 167 168 if (pos >= 0) { 169 // Close all the open elements, up the stack 170 for (var i = stack.length - 1; i >= pos; i--) 171 if (handler.end) 172 handler.end(stack[i]); 173 174 // Remove the open elements from the stack 175 stack.length = pos; 176 } 177 } 178 }; 179 180 html = ` 181 <div id="s_wrap" class="s-isindex-wrap"> 182 <div id="s_main" class="main clearfix"> 183 <div id="s_mancard_main" class="s-mancacrd-main"> 184 <div class="s-menu-container"> 185 <div id="s_menu_gurd" class="s-menu-gurd"> 186 <div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8"> 187 <span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100"> 188 <div class="mine-icon"></div> 189 <div class="mine-text">個人關注</div> 190 </span> 191 <div class="s-menus-outer"> 192 <div id="s_menus_wrapper" class="menus-wrapper"></div> 193 <div class="s-bg-space s-opacity-white-background"></div> 194 <span class="s-menu-music" data-id="3"></span> 195 </div> 196 <span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="設置"> 197 <div class="menu-icon"></div> 198 </span> 199 </div> 200 </div> 201 </div> 202 </div> 203 </div> 204 </div> 205 ` 206 207 HTMLParser(html,{ 208 start: function(tag, attrs, unary) { 209 console.log('標籤頭', tag, attrs) 210 }, 211 end: function (tag) { 212 console.log('標籤尾', tag) 213 }, 214 //處理真實的節點 215 chars: function(text) { 216 console.log('標籤字段', text.trim().length > 0 ? text : '空字符' ) 217 } 218 }) 219 220 </script> 221 222 223 </body> 224 </html>
1 html = ` 2 <div id="s_wrap" class="s-isindex-wrap"> 3 <div id="s_main" class="main clearfix"> 4 <div id="s_mancard_main" class="s-mancacrd-main"> 5 <div class="s-menu-container"> 6 <div id="s_menu_gurd" class="s-menu-gurd"> 7 <div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8"> 8 <span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100"> 9 <div class="mine-icon"></div> 10 <div class="mine-text">個人關注</div> 11 </span> 12 <div class="s-menus-outer"> 13 <div id="s_menus_wrapper" class="menus-wrapper"></div> 14 <div class="s-bg-space s-opacity-white-background"></div> 15 <span class="s-menu-music" data-id="3"></span> 16 </div> 17 <span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="設置"> 18 <div class="menu-icon"></div> 19 </span> 20 </div> 21 </div> 22 </div> 23 </div> 24 </div> 25 </div> 26 ` 27 28 HTMLParser(html,{ 29 start: function(tag, attrs, unary) { 30 console.log('標籤頭', tag, attrs) 31 }, 32 end: function (tag) { 33 console.log('標籤尾', tag) 34 }, 35 //處理真實的節點 36 chars: function(text) { 37 console.log('標籤字段', text.trim().length > 0 ? text : '空字符' ) 38 } 39 })
這裏使用HTMLParser,很容易就能夠把html模板解析爲element樹
咱們很容易就能夠將一根dom結構用js對象來抽象,好比咱們這裏的班次列表排序:
這裏出發的因子就有出發時間、耗時、價格,這裏表示下就是:
let trainData = { sortKet: 'time', //耗時,價格,發車時間等等方式排序 sortType: 1, //1升序,2倒敘 oData: [], //服務器給過來的原生數據 data: [], //當前篩選條件下的數據 }
這個對象有個缺陷就是不能與頁面映射起來,咱們須要在代碼中維護數據與試圖的映射關係(data與dom的關係),一旦數據發生變化便從新渲染。比較複雜的問題是半年後這個頁面的維護者三易其手,而篩選條件增長、業務邏輯變化,這個頁面的代碼可能會變得至關難維護,其中最難的點可能就是頁面中的dom關係和事件維護
而咱們想要的就是數據改變了,DOM本身就發生變化,而且以高效的方式發生變化,這個就是咱們snabbdom作的工做了,咱們用一段代碼說明這個問題:
var element = { tagName: 'ul', // 節點標籤名 props: { // DOM的屬性,用一個對象存儲鍵值對 id: 'list' }, children: [ // 該節點的子節點 {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}, ] }
這個映射成dom結構就是:
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>
真實的VNode會翻譯爲這樣:
class Element { constructor(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; } } function el(tagName, props, children) { return new Element(tagName, props, children) } el('ul', {id: 'list'}, [ el('li', {class: 'item'}, ['Item 1']), el('li', {class: 'item'}, ['Item 2']), el('li', {class: 'item'}, ['Item 3']) ])
這裏很快就能封裝一個可運行的代碼出來:
//***虛擬dom部分代碼,後續會換成snabdom class Element { constructor(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; } render() { //拿着根節點往下面擼 let root = document.createElement(this.tagName); let props = this.props; for(let name in props) { root.setAttribute(name, props[name]); } let children = this.children; for(let i = 0, l = children.length; i < l; i++) { let child = children[i]; let childEl; if(child instanceof Element) { //遞歸調用 childEl = child.render(); } else { childEl = document.createTextNode(child); } root.append(childEl); } this.rootNode = root; return root; } } function el(tagName, props, children) { return new Element(tagName, props, children) } let vnode = el('ul', {id: 'list'}, [ el('li', {class: 'item'}, ['Item 1']), el('li', {class: 'item'}, ['Item 2']), el('li', {class: 'item'}, ['Item 3']) ]) let root = vnode.render(); document.body.appendChild(root);
snabbdom作的事情,即是把這段代碼寫的更加完善一點,而且處理裏面最爲複雜的比較兩顆虛擬樹的差別了,而這塊也是snabbdom的核心,固然也比較有難度啦,咱們這裏能用就行便不深刻了,這裏來一段代碼說明下snabbdom的使用:
var snabbdom = require("snabbdom"); var patch = snabbdom.init([ // 初始化補丁功能與選定的模塊 require("snabbdom/modules/class").default, // 使切換class變得容易 require("snabbdom/modules/props").default, // 用於設置DOM元素的屬性(注意區分props,attrs具體看snabbdom文檔) require("snabbdom/modules/style").default, // 處理元素的style,支持動畫 require("snabbdom/modules/eventlisteners").default, // 事件監聽器 ]); //h是一個生成vnode的包裝函數,factory模式?對生成vnode更精細的包裝就是使用jsx //在工程裏,咱們一般使用webpack或者browserify對jsx編譯 var h = require("snabbdom/h").default; // 用於建立vnode,VUE中render(createElement)的原形 var container = document.getElementById("container"); var vnode = h("div#container.two.classes", {on: {click: someFn}}, [ h("span", {style: {fontWeight: "bold"}}, "This is bold"), " and this is just normal text", h("a", {props: {href: "/foo"}}, "I\"ll take you places!") ]); // 第一次打補丁,用於渲染到頁面,內部會創建關聯關係,減小了建立oldvnode過程 patch(container, vnode); //建立新節點 var newVnode = h("div#container.two.classes", {on: {click: anotherEventHandler}}, [ h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"), " and this is still just normal text", h("a", {props: {href: "/bar"}}, "I\"ll take you places!") ]); //第二次比較,上一次vnode比較,打補丁到頁面 //VUE的patch在nextTick中,開啓異步隊列,刪除了沒必要要的patch //nextTick異步隊列解析,下面文章中會詳解 patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
繼續來一段例子作說明:
<div id="container"> </div> <script type="module"> "use strict"; import { patch, h, VNode } from './libs/vnode.js' var container = document.getElementById("container"); function someFn(){ console.log(1)} function anotherEventHandler(){ console.log(2)} var oldVnode = h("div", {on: {click: someFn}}, [ h("span", {style: {fontWeight: "bold"}}, "This is bold"), " and this is just normal text", h("a", {props: {href: "/foo"}}, "I\"ll take you places!") ]); // 第一次打補丁,用於渲染到頁面,內部會創建關聯關係,減小了建立oldvnode過程 let diff = patch(container, oldVnode); //建立新節點 var newVnode = h("div", {on: {click: anotherEventHandler}}, [ h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"), " and this is still just normal text", h("a", {props: {href: "/bar"}}, "I\"ll take you places!") ]); //第二次比較,上一次vnode比較,打補丁到頁面 //VUE的patch在nextTick中,開啓異步隊列,刪除了沒必要要的patch //nextTick異步隊列解析,下面文章中會詳解 patch(oldVnode, newVnode); // Snabbdom efficiently updates the old view to the new state function test() { return { oldVnode,newVnode,container,diff } } </script>
snabbdom在組件系統中的應用
MVVM系統還有個比較關鍵的是組件系統,通常認爲MVVM的兩大特色實際上是響應式數據更新(VNode相關),而後就是組件體系,這二者須要完成的工做都是讓咱們更高效的開發代碼,一個爲了解決紛亂的dom操做,一個爲了解決負責的業務邏輯結構,而組件體系便會用到snabbdom中的hook:
//建立組件 //子組件option,屬性,子元素,tag _createComponent(Ctor, data, children, sel) { Ctor.data = mergeOptions(Ctor.data); let componentVm; let Factory = this.constructor let parentData = this.$data data.hook.insert = (vnode) => { //... } Ctor._vnode = new VNode(sel,null,data, [], undefined, createElement(sel)); return Ctor._vnode }
使用通常流程,咱們不會解析這個組件而是插入沒有意義的標籤:
<my-component></my-component> <div m-for="(val, key, index) in arr">索引 1 :葉小釵</div> <div m-for="(val, key, index) in arr">索引 2 :素還真</div> <div m-for="(val, key, index) in arr">索引 3 :一頁書</div>
1 _createComponent(Ctor, data, children, sel) { 2 Ctor.data = mergeOptions(Ctor.data); 3 let componentVm; 4 let Factory = this.constructor 5 let parentData = this.$data 6 data.hook.insert = (vnode) => { 7 Ctor.data = Ctor.data || {}; 8 var el =createElement('sel') 9 vnode.elm.append(el) 10 Ctor.el = el; 11 componentVm = new Factory(Ctor); 12 vnode.key = componentVm.uid; 13 componentVm._isComponent = true 14 componentVm.$parent = this; 15 (this.$children || (this.$children = [])).push(componentVm); 16 //寫在調用父組件值 17 for (let key in data.attrs) { 18 if (Ctor.data[key]) { 19 warn(`data:${key},已存在`); 20 continue; 21 } 22 } 23 } 24 Ctor._vnode = new VNode(sel,null,data, [], undefined, createElement(sel)); 25 return Ctor._vnode 26 }
可是咱們爲snabbdom設置了一個hook(鉤子),當標籤被插入的時候會執行這段邏輯(加粗部分代碼),這裏先建立了一個空標籤(sel)直接插入my-component中,而後執行與以前同樣的實例化流程:
componentVm = new Factory(Ctor);
這個會在patch後將實際的dom節點更新上去:
this.$el = patch(this.$el, vnode); //$el如今爲sel標籤(dom標籤)
這個就是snabbdom hook所幹的工做,同時能夠看到組件系統這裏有這些特色:
① 組件是一個獨立的mvvm實例,經過parent能夠找到其父親mvvm實例,可能跟實例,也多是另外一個組件
② 根實例能夠根據$children參數找到其下面全部的組件
③ 組件與跟實例經過data作交流,原則不容許在組件內部改變屬性值,須要使用事件進行通訊,事件通訊就是在組件中的點擊事件不作具體的工做,而是釋放$emit(),這種東西讓跟實例調用,最終仍是以setData的方式改變基本數據,從而引起組件同步更新
能夠看到,只要利用好了HTMLParser以及snabbdom兩大神器,咱們的框架代碼變化簡單許多,而瞭解了這兩大神器的使用後,再去讀Vue的源碼可能也會簡單流暢一些
前段時間,咱們由於想要統一小程序&web&Native端的代碼作了一些研究,而且模仿着實現了一個簡單缺漏的mvvm框架,這樣的過程當中,咱們抓住了mvvm框架的基本脈絡,接下來咱們看看mpvue是怎麼作的,而後再繼續咱們後續的研究
對應Git代碼地址請見:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
https://github.com/fastCreator/MVVM(極度參考,十分感謝該做者,直接看Vue會比較吃力的,可是看完這個做者的代碼便會輕易不少,惋惜這個做者沒有對應博客說明,否則就爽了)
https://www.tangshuang.net/3756.html
https://www.cnblogs.com/kidney/p/8018226.html
http://www.cnblogs.com/kidney/p/6052935.html
https://github.com/livoras/blog/issues/13