對應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
之前我們完成了簡陋的從模板到虛擬DOM從虛擬DOM到HTML的代碼,我們這里圖簡單沒有對屬性和樣式做特殊處理,還是按照一般的模板方式進行的解析,后續看看這塊怎么處理吧,今天我們的任務是完成setData時候同步更新我們的HTML的操作,這里首先我們來看看一般的MVVM中數據變化更新是怎么完成的,在這個基礎上進行后續的代碼可能各位看得更清晰。
一般的MVVM雙向綁定
一般來說,我們數據變化的時候都是一個發布訂閱模式,我們調用setData的時候會執行類似這樣的代碼:
1 function setData(data) { 2 //做下數據變更 3 //...... 4 5 //會通知對應數據對象數據發生變化了,這個數據對應的所有dom節點都會發生改變 6 this.notifyAll(); 7 }
而在vue中我們是直接做這種操作,dom就發生了變化:
this.name = '葉小釵';
這個是因為,他使用了訪問器屬性:
1 var obj = { }; 2 // 為obj定義一個名為 name 的訪問器屬性 3 Object.defineProperty(obj, "name", { 4 5 get: function () { 6 console.log('get', arguments); 7 }, 8 set: function (val) { 9 console.log('set', arguments); 10 } 11 }) 12 obj.name = '葉小釵' 13 console.log(obj, obj.name) 14 /* 15 set Arguments ["葉小釵", callee: ƒ, Symbol(Symbol.iterator): ƒ] 16 get Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ] 17 */
如果這里寫這樣的代碼:
1 <div id="a"> 2 </div> 3 <input type="text" id="b"> 4 5 <script type="text/javascript" > 6 7 function setData(data) { 8 //做下數據變更 9 //...... 10 //會通知對應數據對象數據發生變化了,這個數據對應的所有dom節點都會發生改變 11 this.notifyAll(); 12 } 13 14 function getElById(id) { 15 return document.getElementById(id); 16 } 17 18 var obj = {}; 19 // 為obj定義一個名為 name 的訪問器屬性 20 Object.defineProperty(obj, "name", { 21 set: function (val) { 22 getElById('a').innerHTML = val; 23 getElById('b').value = val; 24 } 25 }) 26 27 getElById('b').addEventListener('input', function(e) { 28 obj.name = e.target.value; 29 }); 30 31 </script>
文本框中的字符串和div的便會同步更新,這個便是最簡化的雙向綁定代碼了,真實情況下我們的代碼可能是這樣的:
① 將data中的數據(這里是name屬性),與兩個dom對象進行映射一個是input另一個是空字符串(可以想象為span)
② 當data中name字段發生變化,或者view中導致name發生變化(控制台或者事件監聽)
③ data數據變化時,文本節點同步發生變化(不管是控制台js腳本導致還是輸入變化)
PS:我們這里與小程序保持一致,真正做更新時候采用setData方法進行
這里便開始引入編譯過程:
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 compile(node, vm) { 14 let reg = /\{\{(.*)\}\}/; 15 16 //節點類型 17 if(node.nodeType === 1) { 18 let attrs = node.attributes; 19 //解析屬性 20 for(let i = 0, l = attrs.length; i < l; i++) { 21 if(attrs[i].nodeName === 'v-model') { 22 let name = attrs[i].nodeValue; 23 node.value = vm.data[name] || ''; 24 //此處不做太多判斷,直接綁定事件 25 node.addEventListener('input', function (e) { 26 //賦值操作 27 let newObj = {}; 28 newObj[name] = e.target.value; 29 vm.setData(newObj); 30 }); 31 32 break; 33 } 34 } 35 } else if(node.nodeType === 3) { 36 37 if(reg.test(node.nodeValue)) { 38 let name = RegExp.$1; // 獲取匹配到的name 39 name = name.trim(); 40 node.nodeValue = vm.data[name] || ''; 41 } 42 } 43 } 44 45 //獲取節點 46 function nodeToFragment(node, vm) { 47 let flag = document.createDocumentFragment(); 48 let child; 49 50 while (child = node.firstChild) { 51 compile(child, vm); 52 flag.appendChild(child); 53 } 54 55 return flag; 56 } 57 58 function MVVM(options) { 59 this.data = options.data; 60 let el = getElById(options.el); 61 this.$dom = nodeToFragment(el, this) 62 this.$el = el.appendChild(this.$dom); 63 64 // this.$bindEvent(); 65 } 66 67 MVVM.prototype.setData = function (data) { 68 for(let k in data) { 69 this.data[k] = data[k]; 70 } 71 //執行更新邏輯 72 } 73 74 let mvvm = new MVVM({ 75 el: 'app', 76 data: { 77 name: '葉小釵' 78 } 79 }) 80 81 </script>
這個時候input輸入更改,對應屬性也會發生變化,但是我們屬性發生變化並沒有引起所有的dom發生變化,這個是不對的,這里我們便需要劫持所有的數據對象,這里引入發布訂閱模式:
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 </script>
mvvm.setData({name: 'hello world'})
這段短短的代碼,基本將數據變化如何引起的dom變化說的比較清楚了,幾個關鍵流程是:
① 設置全局的發布訂閱模式
② 在模板編譯的時候,一旦碰到數據節點與dom節點發生關系時,則新增一個訂閱者,我們這里的發布者沒有狀態概念,真實的情況應該是以data為一個集合的分組,這樣可以做到安data進行更新
③ 數據變化時候執行setData,底層調用發布者除非對應訂閱者更新數據,這里只是簡單的屬性&文本更新,真實情況會復雜的多,我們這里為保持小程序邏輯,沒有實現訪問器屬性部分代碼
有了以上代碼的理解,我們再回到我們昨天的代碼繼續完成這個流程便會清晰的多
完成setData代碼
根據之前的學習,我們知道添加訂閱者一定是發生在編譯時期,data跟node產生關聯的時候,但是我們這里需要發布訂閱者相關代碼,由於我們這里的訴求還要簡單一些並不想去考慮屬性樣式這些特殊性,所以我們對TextParser做點改造,先實現之:
注意這里的核心是,每次數據改變的時候都會觸發觀察者的update,這樣會引起重新生成虛擬樹(vnode),但是到底要不要重新渲染,怎么渲染后面會直接由snabbdom接手,我們只是將這種關系完成,代碼比較分散大家可以到github上面看:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
然后今天的學習到此為止,我們明天開始處理事件部分的代碼,感覺代碼逐漸有些慢了,等組件部分完成后我們畫點流程圖重新梳理下邏輯