詳解Vue雙向數(shù)據(jù)綁定原理解析
基本原理
Vue.采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter和getter,數(shù)據(jù)變動時發(fā)布消息給訂閱者,觸發(fā)相應(yīng)函數(shù)的回調(diào)。
思路整理
要實現(xiàn)mvvm的雙向綁定,需要實現(xiàn)如下幾點:
1.實現(xiàn)一個數(shù)據(jù)監(jiān)聽器Observer,能夠?qū)ο蟮乃袑傩赃M行監(jiān)聽,發(fā)生變化時拿到最新值通知訂閱者
2.實現(xiàn)一個解析器Compile,對每個子元素節(jié)點的指令進行掃描和解析,根據(jù)模板指令替換數(shù)據(jù),初始化視圖以及綁定相應(yīng)的回調(diào)函數(shù);
3.實現(xiàn)一個Watcher,作為Observer和Compile的橋梁,能夠訂閱屬性變動的通知,執(zhí)行指令綁定的回調(diào)函數(shù),更新視圖
4.mvvm的入口,整合以上三者
流程圖如下:

分布實現(xiàn)
1. MVVM.js
function MVVM(options) {
this.$options = options || {};
var data = this._data = this.$options.data;
var me = this;
// 數(shù)據(jù)代理
// 實現(xiàn) vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key) {
me._proxyData(key);
});
// 代理計算屬性
// 同樣通過Object.defineProperty進行劫持
this._initComputed();
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
$watch: function(key, cb, options) {
new Watcher(this, key, cb);
}
}
MVVM入口文件,整合Observer/Compile/Watcher三者,達到數(shù)據(jù)變化->更新視圖;視圖變化->數(shù)據(jù)變更的雙向綁定效果。(結(jié)合鉤子函數(shù),理解Vue生命周期中各個階段的作用)
2. Observer.js
function Observer(data) {
Object.keys(data).forEach(function() {
defineReactive(data, key, data[key]);
});
}
function defineReactive (data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚舉
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的話,進行監(jiān)聽
childObj = observe(newVal);
// 通知訂閱者
dep.notify();
}
});
}
對需要監(jiān)測的對象的每個屬性進行遞歸遍歷,通過Object.defineProperty設(shè)置setter和getter。當(dāng)設(shè)置新的屬性值時,觸發(fā)相應(yīng)的setter,通知訂閱者。
function Dep() {
this.id = uid++;
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
depend: function() {
Dep.target.addDep(this);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
訂閱者模式,每個屬性維護一個Dep,記錄自己的訂閱者(即watcher),notify通知每個訂閱者執(zhí)行相應(yīng)的update方法,更新視圖。
3. Compile.js
Compile做了兩件事情:
1.解析模板指令,替換變量,初始化渲染視圖;
2.生成一個watcher,注冊回調(diào)函數(shù),添加監(jiān)聽數(shù)據(jù)的訂閱者,數(shù)據(jù)變動時,更新視圖

解析流程如下:
1.將DOM轉(zhuǎn)成文檔碎片fragment,提升查詢效率
2.遍歷所有元素節(jié)點及其子節(jié)點,調(diào)用對應(yīng)的指令渲染函數(shù)渲染,并調(diào)用對應(yīng)的指令更新函數(shù)進行綁定
3.將fragment添加回真實的DOM中
遍歷元素
function compileElement (el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
// 解析元素節(jié)點
if (me.isElementNode(node)) {
me.compile(node);
// {{}}替換變量
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1);
}
// 遞歸遍歷子節(jié)點
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
}
編譯元素節(jié)點
compile: function(node) {
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// 指令以v-xxx命名
// <span v-html="content"></span>
var attrName = attr.name; // v-html
if (me.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2);
// 事件指令
if (me.isEventDirective(dir)) {
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
node.removeAttribute(attrName);
}
});
}
指令處理與更新函數(shù)
var compileUtil = {
html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 第一次初始化視圖
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 實例化Watcher,添加訂閱者
new Watcher(vm, exp, function(value, oldValue) {
// 屬性變化的視圖更新函數(shù)
updaterFn && updaterFn(node, value, oldValue);
});
},
}
var Updater = {
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
}
}
4. Watcher.js
Watcher作為Observer與Compile之間通信的橋梁,屬性變化的訂閱者,做了如下的事情:
1.自身實例化時在屬性訂閱器集合dep里添加自己
2.自身需有update方法
3.調(diào)用dep.notice時,watcher調(diào)用自身的update ,觸發(fā)Compile中定義的回調(diào)
function Watcher(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.get();
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this;
var value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}
};
這里需要注意的點是,實例化watcher的時候,調(diào)用get方法,通過Dep.target = curInstance,強行觸發(fā)獲屬性值的getter方法,在屬性的訂閱器中添加當(dāng)前watcher實例。
小結(jié)
雙向綁定的原理很簡單,通過數(shù)據(jù)劫持,當(dāng)設(shè)置新屬性值的時候通過訂閱者更新視圖;編譯指令,替換變量,同時綁定更新函數(shù)到訂閱者;對應(yīng)事件綁定調(diào)用addEventListener進行監(jiān)聽。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
解決antd 表單設(shè)置默認值initialValue后驗證失效的問題
這篇文章主要介紹了解決antd 表單設(shè)置默認值initialValue后驗證失效的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
vue在使用element組件出現(xiàn)<el-input>標(biāo)簽無法輸入的問題
這篇文章主要介紹了vue在使用element組件出現(xiàn)<el-input>標(biāo)簽無法輸入的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04
unplugin-svg-component優(yōu)雅使用svg圖標(biāo)指南
這篇文章主要為大家介紹了unplugin-svg-component優(yōu)雅使用svg圖標(biāo)指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
詳解TypeScript+Vue 插件 vue-class-component的使用總結(jié)
這篇文章主要介紹了TypeScript+Vue 插件 vue-class-component的使用總結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02
Vue.js學(xué)習(xí)記錄之在元素與template中使用v-if指令實例
這篇文章主要給大家介紹了關(guān)于Vue.js學(xué)習(xí)記錄之在元素與template中使用v-if指令的相關(guān)資料,文中給出了詳細的示例代碼供大家參考學(xué)習(xí),相信對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-06-06
vue中echarts圖表大小適應(yīng)窗口大小且不需要刷新案例
這篇文章主要介紹了vue中echarts圖表大小適應(yīng)窗口大小且不需要刷新案例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07
vue.js 實現(xiàn)v-model與{{}}指令方法
這篇文章主要介紹了vue.js 實現(xiàn)v-model與{{}}指令方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10

