vue雙向綁定原理
- 原理主要通過數據劫持和發布訂閱模式實現的
- 通過
Object.defineProperty()來劫持各個屬性的setter,getter,監聽數據的變化 - 在數據變動時發布消息給訂閱者(watcher),訂閱者觸發響應的回調(update)更新視圖。
一、什么是數據劫持
- 訪問或者修改對象的某個屬性時,都會觸發相對應的函數,在這個函數里進行額外的操作或者修改返回結果。
- 在觸發函數的時候,在函數中所做的操作,就是劫持操作。
Object.defineProperty
語法:
- Object.defineProperty(obj,prop,descriptor)
參數:
-
obj:目標對象
-
prop:需要定義的屬性或方法的名稱
-
descriptor:目標屬性所擁有的特性
- value:屬性的值
- writable:如果為false,屬性的值就不能被重寫。
- get:一旦目標屬性被訪問就會調回此方法,並將此方法的運算結果返回用戶。
- set:一旦目標屬性被賦值,就會調回此方法。
- configurable:如果為false,則任何嘗試刪除目標屬性或修改屬性性以下特性(writable, configurable, enumerable)的行為將被無效化。
- enumerable:是否能在for...in循環中遍歷出來或在Object.keys中列舉出來。
使用:
var info = {
name:'hhh'
}
Object.keys(info).forEach(function(key){
Object.defineProperty(info,key,{
enumerable:true, // 是否能在for...in循環中遍歷出來或在Object.keys中列舉出來。
configurable:true, // false,不可修改、刪除目標屬性或修改屬性性以下特性
get:function(){
console.log('被訪問了調用get');
},
set:function(){
console.log('被設置了調用set');
}
})
})
/*
控制台:
輸入:info.name
打印:被訪問了調用get
輸入:info.name = 'hjj'
打印:被設置了調用set
*/
二、實現最簡單的雙向綁定
<div id="demo"></div>
<input type="text" id="inp">
<script>
var info = {};
var demo = document.querySelector('#demo')
var inp = document.querySelector('#inp')
Object.defineProperty(info, 'name', {
get: function() {
return val;
},
set: function (newVal) {//當該屬性被賦值的時候觸發
inp.value = newVal;
demo.innerHTML = newVal;
}
})
inp.addEventListener('input', function(e) {
// 給obj的name屬性賦值,進而觸發該屬性的set方法
info.name = e.target.value;
});
info.name = 'hhhhh';//在給obj設置name屬性的時候,觸發了set這個方法
</script>
二、vue如何實現
2.1.原理圖

2.2.observer
-
observer用來實現對每個vue中的data中定義的屬性循環用Object.defineProperty()實現數據劫持,以便利用其中的setter和getter
-
為每個屬性都分配一個訂閱者集合的管理者—dep,負責記錄和通知訂閱者
-
當數據發生變化時發出一個notice(預告),通知訂閱者,訂閱者會觸發它的update方法,對視圖進行更新。
2.3.compile
-
在vue中v-model,v-bind,{{}}等都可以對數據進行顯示,假如一個屬性都通過這三個指令了,那么每當這個屬性改變的時候,相應的這個三個指令的html視圖也必須改變,
-
於是vue中就是每當有這樣的可能用到雙向綁定的指令,就在一個Dep中增加一個訂閱者,其訂閱者只是更新自己的指令對應的數據,也就是v-model='name'和{{name}}有兩個對應的訂閱者,各自管理自己的地方。
-
每當屬性的set方法觸發,就循環更新Dep中的訂閱者。
三、vue代碼實現
3.1.observer實現
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;
// 作為發布者發出通知,通知后dep會循環調用各自的update方法更新視圖
dep.notify();
}
})
}
//遍歷,對每個屬性進行Object.defineProperty(),並添加至dep中,每個屬性都new了一個Dep(訂閱者集合的管理數組)
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
3.2.實現compile
compile指令解析器,對每個元素節點的指令進行掃描和解析,目的就是解析各種模板指令替換成對數據。
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');
}
}
}
}
3.3.watcher實現
作為連接Observer和Compile的一個中介點,在接收數據變更的同時,讓Dep添加當前Watcher,並及時通知視圖進行update。
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
}
}
3.4.實現Dep來為每個屬性添加訂閱者
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
四、梳理
- 首為每個vue屬性用Object.defineProperty()實現數據劫持
- 為每個屬性分配一個訂閱者集合的管理數組dep
- 然后在編譯的時候在該屬性的數組dep中添加訂閱者,v-model會添加一個訂閱者,{{}}也會,v-bind也會,只要用到該屬性的指令理論上都會
- 接着為input會添加監聽事件,修改值就會為該屬性賦值,觸發該屬性的set方法
- 在set方法內通知訂閱者數組dep,訂閱者數組循環調用各訂閱者的update方法更新視圖。
轉自:
vue雙向綁定原理分析
