vue雙向繫結原理分析
當我們學習angular或者vue的時候,其雙向繫結為我們開發帶來了諸多便捷,今天我們就來分析一下vue雙向繫結的原理。
簡易vue原始碼地址:https://github.com/jiangzhenfei/simple-Vue
1.vue雙向繫結原理
vue.js 則是採用資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()
來劫持各個屬性的setter
,getter
,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。我們先來看Object.defineProperty()這個方法:
var obj = {}; Object.defineProperty(obj, 'name', { get: function() { console.log('我被獲取了') return val; }, set: function (newVal) { console.log('我被設定了') } }) obj.name = 'fei';//在給obj設定name屬性的時候,觸發了set這個方法 var val = obj.name;//在得到obj的name屬性,會觸發get方法
已經瞭解到vue是通過資料劫持的方式來做資料繫結的,其中最核心的方法便是通過Object.defineProperty()
來實現對屬性的劫持,那麼在設定或者獲取的時候我們就可以在get或者set方法裡假如其他的觸發函式,達到監聽資料變動的目的,無疑這個方法是本文中最重要、最基礎的內容之一。
2.實現最簡單的雙向繫結
我們知道通過Object.defineProperty()可以實現資料劫持,是的屬性在賦值的時候觸發set方法,
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="demo"></div> <input type="text" id="inp"> <script> var obj = {}; var demo = document.querySelector('#demo') var inp = document.querySelector('#inp') Object.defineProperty(obj, 'name', { get: function() { return val; }, set: function (newVal) {//當該屬性被賦值的時候觸發 inp.value = newVal; demo.innerHTML = newVal; } }) inp.addEventListener('input', function(e) { // 給obj的name屬性賦值,進而觸發該屬性的set方法 obj.name = e.target.value; }); obj.name = 'fei';//在給obj設定name屬性的時候,觸發了set這個方法 </script> </body> </html>
當然要是這麼粗暴,肯定不行,效能會出很多的問題。
3.講解vue如何實現
先看原理圖
3.1 observer用來實現對每個vue中的data中定義的屬性迴圈用Object.defineProperty()實現資料劫持,以便利用其中的setter和getter,然後通知訂閱者,訂閱者會觸發它的update方法,對檢視進行更新。
3.2 我們介紹為什麼要訂閱者,在vue中v-model,v-name,{{}}等都可以對資料進行顯示,也就是說假如一個屬性都通過這三個指令了,那麼每當這個屬性改變的時候,相應的這個三個指令的html檢視也必須改變,於是vue中就是每當有這樣的可能用到雙向繫結的指令,就在一個Dep中增加一個訂閱者,其訂閱者只是更新自己的指令對應的資料,也就是v-model='name'和{{name}}有兩個對應的訂閱者,各自管理自己的地方。每當屬性的set方法觸發,就迴圈更新Dep中的訂閱者。
4.vue程式碼實現
4.1 observer實現,主要是給每個vue的屬性用Object.defineProperty(),程式碼如下:
function defineReactive (obj, key, val) { var dep = new Dep(); Object.defineProperty(obj, key, { get: function() { //新增訂閱者watcher到主題物件Dep if(Dep.target) { // JS的瀏覽器單執行緒特性,保證這個全域性變數在同一時間內,只會有同一個監聽器使用 dep.addSub(Dep.target); } return val; }, set: function (newVal) { if(newVal === val) return; val = newVal; console.log(val); // 作為釋出者發出通知 dep.notify();//通知後dep會迴圈呼叫各自的update方法更新檢視 } }) } function observe(obj, vm) { Object.keys(obj).forEach(function(key) { defineReactive(vm, key, obj[key]); }) }
4.2實現compile:
compile的目的就是解析各種指令稱真正的html。
function Compile(node, vm) { if(node) { this.$frag = this.nodeToFragment(node, vm); return this.$frag; } } Compile.prototype = { nodeToFragment: function(node, vm) { var self = this; var frag = document.createDocumentFragment(); var child; while(child = node.firstChild) { console.log([child]) self.compileElement(child, vm); frag.append(child); // 將所有子節點新增到fragment中 } return frag; }, compileElement: function(node, vm) { var reg = /\{\{(.*)\}\}/; //節點型別為元素(input元素這裡) if(node.nodeType === 1) { var attr = node.attributes; // 解析屬性 for(var i = 0; i < attr.length; i++ ) { if(attr[i].nodeName == 'v-model') {//遍歷屬性節點找到v-model的屬性 var name = attr[i].nodeValue; // 獲取v-model繫結的屬性名 node.addEventListener('input', function(e) { // 給相應的data屬性賦值,進而觸發該屬性的set方法 vm[name]= e.target.value; }); new Watcher(vm, node, name, 'value');//建立新的watcher,會觸發函式向對應屬性的dep陣列中新增訂閱者, } }; } //節點型別為text if(node.nodeType === 3) { if(reg.test(node.nodeValue)) { var name = RegExp.$1; // 獲取匹配到的字串 name = name.trim(); new Watcher(vm, node, name, 'nodeValue'); } } } }
4.3 watcher實現
function Watcher(vm, node, name, type) { Dep.target = this; this.name = name; this.node = node; this.vm = vm; this.type = type; this.update(); Dep.target = null; } Watcher.prototype = { update: function() { this.get(); this.node[this.type] = this.value; // 訂閱者執行相應操作 }, // 獲取data的屬性值 get: function() { console.log(1) this.value = this.vm[this.name]; //觸發相應屬性的get } }
4.4 實現Dep來為每個屬性新增訂閱者
function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }) } }
這樣一來整個資料的雙向繫結就完成了。
5.梳理
首先我們為每個vue屬性用Object.defineProperty()實現資料劫持,為每個屬性分配一個訂閱者集合的管理陣列dep;然後在編譯的時候在該屬性的陣列dep中新增訂閱者,v-model會新增一個訂閱者,{{}}也會,v-bind也會,只要用到該屬性的指令理論上都會,接著為input會新增監聽事件,修改值就會為該屬性賦值,觸發該屬性的set方法,在set方法內通知訂閱者陣列dep,訂閱者陣列迴圈呼叫各訂閱者的update方法更新檢視。