接觸Vue有一段時間了,可是對於其雙向綁定的實現一直是似懂非懂,今天看到一篇寫的比較好的文章 傳送門1 根據原做者的指導本身也去實現了一遍簡單的 demo (本文的demo均基於Object.defineProperty 實現數據劫持,利用了對Vue.js實現雙向綁定的思想)html
[注]本文全部圖片均來自於:傳送門2vue
1.發佈-訂閱模式
2.髒值檢測node
經過對比數據是否有變動,來決定是否更新視圖。最簡單的能夠經過定時輪詢去檢測數據的變更。固然Google不會這麼low, Angular 只有在指定事件觸發時進入髒值檢測:git
3.數據劫持
Vue.js 採用的是 數據劫持+發佈/訂閱模式
的方式,經過 Object.defineProperty()
來劫持各個屬性的 setter/getter, 在數據變更時發佈消息給訂閱者(Wacther), 觸發相應的監聽回調。下圖展現了Vue實現雙向綁定的流程
github
<!DOCTYPE html> <html lang="en"> <head> <title>雙向綁定最最最初級demo</title> <meta charset="UTF-8"> </head> <body> <div id="app"> <input type="text" id="txt"> <p id="show-txt"></p> </div> </body> <script> var obj={} Object.defineProperty(obj,'txt',{ get:function(){ return obj }, set:function(newValue){ document.getElementById('txt').value = newValue document.getElementById('show-txt').innerHTML = newValue } }) document.addEventListener('keyup',function(e){ obj.txt = e.target.value }) </script> </html>
DOM操做是很是耗時和好性能,因此在優化過程當中先從DOM操做入手。由於遍歷解析過程當中有屢次DOM操做,爲了提升性能和效率,須要一種方法來避免對DOM元素的直接封裝操做。在Vue使用 DocumentFragment
做爲替代容器。DocumentFragment
接口表示一個沒有父級文件的最小文檔對象。它被當作一個輕量版本的Document 使用。因此使用 DocumentFragment 代替DOM直接處理,能夠提升性能和速度。瀏覽器
//將傳入 node 的子節點進行劫持,通過處理後從新掛載回目標節點 function convertNode(node,vm){ var fragment = document.createDocumentFragment(), child while(child = node.firstChild){ //將原生節點拷貝到 fragment,並刪除以前的child節點 fragment.appendChild(child) } return fragment } var dom = convertNode(document.getElementById('app')) document.getElmentById('app').appendChild(dom)
Complie 主要作的事情就是解析模板指令,將模板中的變量替換爲數據。因此要遍歷整個DOM樹,進行掃描解析編譯,調用對應的指令渲染函數進行渲染,並調用對應的指令更新函數進行綁定app
<div id="app"> <input type="text" id="txt" h-model="text"> {{text}} </div>
function convertNode(node,vm){ //... while(child = node.firstChild){ Compile(child,vm) fragment.appendChild(child) } return fragment } function Compile(node,vm){ var reg = /\{\{(.*)\}\}/ if(node.nodeType===1){ var attr = node.attributes //對全部屬性進行解析 for(var i=0;i<attr.length;i++){ if(attr[i].nodeName=='h-model'){ //將元素與數據綁定 var bindName = attr[i].nodeValue //爲輸入框添加事件監聽觸發 node.addEventListener('input',function(e){ vm.data[bindName] = e.target.value node.value = vm.data[bindName] }) node.removeAttribute('h-model'); } if(node.nodeType===3){ if(reg.test(node.nodeValue)){ var bindName = RegExp.$1.trim() console.log(RegExp.$1) node.nodeValue = vm.data[bindName] } } } }
接下來實現一個 Xin
構造器,經過 Compile 來解析模板指令,經過 Observer 監聽屬性數據的變化實現 Model
層向 View
層的數據綁定dom
function Xin(options){ this.data = options.data Observer(this.data,this) var id = options.el var dom = convertNode(document.getElementById(id),this) document.getElementById(id).appendChild(dom) }
新建一個 vm 實例來測試一下 Model --> View
的綁定狀況mvvm
var vm = new Xin({ el:'app', data:{ text:'Hello MVVM' } })
實際上,在 Observer 中咱們已經經過 數據劫持
實現了監聽每一個數據的變化,在控制檯打印 console.log(val)
就能夠實時看到數據的變化。因此接下來實現的關鍵就是怎麼用監聽到的數據去更新視圖。
在這裏,咱們去實現一個 Wacther
能夠將它理解爲觀察者 ,他的做用是可以接收從 Observer 發過來的屬性變更通知, 而後根據屬性的變更更新視圖 update
。函數
在監聽過程當中,爲全部的 data 屬性生成一個主題對象 Dep,Dep中包含須要維護的觀察者列表。每當主題對象狀態發生變化時,其相關依賴都會獲得通知,而且被自動更新(數據變更會觸發notify,再調用訂閱者的update() 方法)
function Dep(){ this.subs=[] //訂閱者隊列 } Dep.prototype={ addSub:function(sub){ this.subs.push(sub) }, notify:function(){ this.subs.forEach(function(sub){ sub.update() }) } } funcion Watcher(vm,node,bindName){ //將全局Dep.target設置爲當前頁面元素node Dep.target = this //完成watcher的初始化 this.name = bindName this.node = node this.vm = vm this.update() //初次綁定時進行更新 Dep.target = null //保證Dep.target惟一 } Watcher.prototype = { get:function(){ this.value = this.vm.data[this.name] }, update:function(){ this.get() this.node.nodeValue = this.value } } function Observer(obj,vm){ //... Object.defineProperty(obj,prop,{ get:function(){...}, set:function(newVal){ if(val == newVal) return val = newVal //data屬性被修改,由dep觸發view層更新 dep.notify() } }) }
考慮這樣一個問題,何時會有雙向綁定? viewModel --> view
可能會發生在全部類型的DOM節點上,而 view --> viewModel
只能發生在 input, select, textarea 等交互控件上。因此將文本節點包裝成 Watcher
, 添加相關元素的觀察者列表中,Watcher 負責更新頁面元素
function Compile(node,vm){ //... if(node.nodeType ===3){ //文本節點類型 if(reg.test(node.nodeValue)){ var bindName = RegExp.$1.trim() new Watcher = (vm,node,bindName) //爲該頁面元素node生產watcher } } }
本文中實現模板渲染的方法借鑑了 Vue 1.x
中實現模板渲染的方法。Vue 2.x
模板渲染 方法借鑑React 中的 VirtualDOM,基於 VirtualDOM。 Vue 2.x 還支持服務端渲染SSR