一套程式碼小程式&Web&Native執行的探索04——資料更新
接上文: ofollow,noindex" target="_blank">一套程式碼小程式&Web&Native執行的探索03
對應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節點都會發生改變 6this.notifyAll(); 7 }
而在vue中我們是直接做這種操作,dom就發生了變化:
this.name = '葉小釵';
這個是因為,他使用了訪問器屬性:
1 var obj = { }; 2 // 為obj定義一個名為 name 的訪問器屬性 3 Object.defineProperty(obj, "name", { 4 5get: function () { 6console.log('get', arguments); 7}, 8set: function (val) { 9console.log('set', arguments); 10} 11 }) 12 obj.name = '葉小釵' 13 console.log(obj, obj.name) 14 /* 15set Arguments ["葉小釵", callee: ƒ, Symbol(Symbol.iterator): ƒ] 16get 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節點都會發生改變 11this.notifyAll(); 12 } 13 14 function getElById(id) { 15return document.getElementById(id); 16 } 17 18 var obj = {}; 19 // 為obj定義一個名為 name 的訪問器屬性 20 Object.defineProperty(obj, "name", { 21set: function (val) { 22getElById('a').innerHTML = val; 23getElById('b').value = val; 24} 25 }) 26 27 getElById('b').addEventListener('input', function(e) { 28obj.name = e.target.value; 29 }); 30 31 </script>
Line"/>
文字框中的字串和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 8function getElById(id) { 9return document.getElementById(id); 10} 11 12//這塊程式碼僅做功能說明,不用當真 13function compile(node, vm) { 14let reg = /\{\{(.*)\}\}/; 15 16//節點型別 17if(node.nodeType === 1) { 18let attrs = node.attributes; 19//解析屬性 20for(let i = 0, l = attrs.length; i < l; i++) { 21if(attrs[i].nodeName === 'v-model') { 22let name = attrs[i].nodeValue; 23node.value = vm.data[name] || ''; 24//此處不做太多判斷,直接繫結事件 25node.addEventListener('input', function (e) { 26//賦值操作 27let newObj = {}; 28newObj[name] = e.target.value; 29vm.setData(newObj); 30}); 31 32break; 33} 34} 35} else if(node.nodeType === 3) { 36 37if(reg.test(node.nodeValue)) { 38let name = RegExp.$1; // 獲取匹配到的name 39name = name.trim(); 40node.nodeValue = vm.data[name] || ''; 41} 42} 43} 44 45//獲取節點 46function nodeToFragment(node, vm) { 47let flag = document.createDocumentFragment(); 48let child; 49 50while (child = node.firstChild) { 51compile(child, vm); 52flag.appendChild(child); 53} 54 55return flag; 56} 57 58function MVVM(options) { 59this.data = options.data; 60let el = getElById(options.el); 61this.$dom = nodeToFragment(el, this) 62this.$el = el.appendChild(this.$dom); 63 64 //this.$bindEvent(); 65} 66 67MVVM.prototype.setData = function (data) { 68for(let k in data) { 69this.data[k] = data[k]; 70} 71//執行更新邏輯 72} 73 74let mvvm = new MVVM({ 75el: 'app', 76data: { 77name: '葉小釵' 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 8function getElById(id) { 9return document.getElementById(id); 10} 11 12//主體物件,儲存所有的訂閱者 13function Dep () { 14this.subs = []; 15} 16 17//通知所有訂閱者資料變化 18Dep.prototype.notify = function () { 19for(let i = 0, l = this.subs.length; i < l; i++) { 20this.subs[i].update(); 21} 22} 23 24//新增訂閱者 25Dep.prototype.addSub = function (sub) { 26this.subs.push(sub); 27} 28 29let globalDataDep = new Dep(); 30 31//觀察者,框架會接觸data的每一個與node相關的屬性, 32//如果data沒有與任何節點產生關聯,則不予理睬 33//實際的訂閱者物件 34//注意,只要一個數據物件對應了一個node物件就會生成一個訂閱者,所以真實通知的時候應該需要做到通知到對應資料的dom,這裡不予關注 35function Watcher(vm, node, name) { 36this.name = name; 37this.node = node; 38this.vm = vm; 39if(node.nodeType === 1) { 40this.node.value = this.vm.data[name]; 41} else if(node.nodeType === 3) { 42this.node.nodeValue = this.vm.data[name] || ''; 43} 44globalDataDep.addSub(this); 45 46} 47 48Watcher.prototype.update = function () { 49if(this.node.nodeType === 1) { 50this.node.value = this.vm.data[this.name ]; 51} else if(this.node.nodeType === 3) { 52this.node.nodeValue = this.vm.data[this.name ] || ''; 53} 54} 55 56//這塊程式碼僅做功能說明,不用當真 57function compile(node, vm) { 58let reg = /\{\{(.*)\}\}/; 59 60//節點型別 61if(node.nodeType === 1) { 62let attrs = node.attributes; 63//解析屬性 64for(let i = 0, l = attrs.length; i < l; i++) { 65if(attrs[i].nodeName === 'v-model') { 66let name = attrs[i].nodeValue; 67if(node.value === vm.data[name]) break; 68 69 //node.value = vm.data[name] || ''; 70new Watcher(vm, node, name) 71 72//此處不做太多判斷,直接繫結事件 73node.addEventListener('input', function (e) { 74//賦值操作 75let newObj = {}; 76newObj[name] = e.target.value; 77vm.setData(newObj, true); 78}); 79 80break; 81} 82} 83} else if(node.nodeType === 3) { 84 85if(reg.test(node.nodeValue)) { 86let name = RegExp.$1; // 獲取匹配到的name 87name = name.trim(); 88 //node.nodeValue = vm.data[name] || ''; 89new Watcher(vm, node, name) 90} 91} 92} 93 94//獲取節點 95function nodeToFragment(node, vm) { 96let flag = document.createDocumentFragment(); 97let child; 98 99while (child = node.firstChild) { 100compile(child, vm); 101flag.appendChild(child); 102} 103 104return flag; 105} 106 107function MVVM(options) { 108this.data = options.data; 109let el = getElById(options.el); 110this.$dom = nodeToFragment(el, this) 111this.$el = el.appendChild(this.$dom); 112 113 //this.$bindEvent(); 114} 115 116MVVM.prototype.setData = function (data, noNotify) { 117for(let k in data) { 118this.data[k] = data[k]; 119} 120//執行更新邏輯 121 //if(noNotify) return; 122globalDataDep.notify(); 123} 124 125let mvvm = new MVVM({ 126el: 'app', 127data: { 128name: '葉小釵' 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
然後今天的學習到此為止,我們明天開始處理事件部分的程式碼,感覺程式碼逐漸有些慢了,等元件部分完成後我們畫點流程圖重新梳理下邏輯