接上文:一套代碼小程序&Web&Native運行的探索03javascript
對應Git代碼地址請見:https://github.com/yexiaochai/wxdemo/tree/master/mvvmhtml
參考:vue
https://github.com/fastCreator/MVVM(極度參考,十分感謝該做者,直接看Vue會比較吃力的,可是看完這個做者的代碼便會輕易不少,惋惜這個做者沒有對應博客說明,否則就爽了)java
https://www.tangshuang.net/3756.htmlnode
https://www.cnblogs.com/kidney/p/8018226.htmlgit
http://www.cnblogs.com/kidney/p/6052935.htmlgithub
https://github.com/livoras/blog/issues/13小程序
以前咱們完成了簡陋的從模板到虛擬DOM從虛擬DOM到HTML的代碼,咱們這裏圖簡單沒有對屬性和樣式作特殊處理,仍是按照通常的模板方式進行的解析,後續看看這塊怎麼處理吧,今天咱們的任務是完成setData時候同步更新咱們的HTML的操做,這裏首先咱們來看看通常的MVVM中數據變化更新是怎麼完成的,在這個基礎上進行後續的代碼可能各位看得更清晰。app
通常的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
而後今天的學習到此爲止,咱們明天開始處理事件部分的代碼,感受代碼逐漸有些慢了,等組件部分完成後咱們畫點流程圖從新梳理下邏輯