對于VUE,最顯著的特點之一就是其數(shù)據(jù)雙向綁定而帶來的奇妙開發(fā)體驗。經(jīng)由vue源碼中的某些操作,使得工程師在項目開發(fā)過程中,無需操作Dom,邏輯層對數(shù)據(jù)的改變便會自動反饋在視圖層;反過來,v-model的使用也會使得用戶在視圖層上的修改映射到真實數(shù)據(jù)上。
vue官方文檔中有一目---”深入響應(yīng)式原理“,專門闡述了這一特性的實現(xiàn)機(jī)制,然而篇幅有限,有些具體點的闡述對初學(xué)者來講還是不是很友好。前一段時間自己專門去找了一些源碼相關(guān)的內(nèi)容去學(xué)習(xí),詳細(xì)了解了一下這一過程。此篇文章將會梳理總結(jié)一下自己的學(xué)習(xí)成果。完整代碼見:https://github.com/cyanl77/mvvm
下面進(jìn)入正題。(持續(xù)更新)
1 概述
1.1 數(shù)據(jù)變化監(jiān)聽
”深入響應(yīng)式原理“第一小節(jié)叫做”如何追蹤變化“,它想要探討的問題和此部分一致,即javascript本身是如何監(jiān)聽到一個數(shù)據(jù)的變化的,了解這一點是理解”響應(yīng)式“機(jī)制的第一步。
實現(xiàn)這一功能的是Object.defineProperty。該方法本身的目的在于定義或修改一個對象的現(xiàn)有屬性,該方法第三個參數(shù)屬性描述符可通過一對函數(shù)getter和setter來定義一個屬性的存取特性,它們分別在該屬性被讀取或重新賦值的時候被調(diào)用?,F(xiàn)在可明確,js即是通過定義待觀測屬性的getter和setter來達(dá)到監(jiān)測其變化,進(jìn)而響應(yīng)變化的目的。
到此,可以寫出如下實現(xiàn)響應(yīng)式系統(tǒng)的雛形,假設(shè)我們要監(jiān)測一個對象中屬性,當(dāng)其發(fā)生改變時,自動在控制臺輸出:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
//do something
return value;
},
set (newValue) {
if(value !== newValue) {
value = newValue;
//do something
}
}
})
1.2 vue中的數(shù)據(jù)依賴收集
vue構(gòu)建的視圖中,可能有多處依賴于data中的同一屬性,當(dāng)邏輯層的值發(fā)生變化,理應(yīng)對視圖層的每一個值進(jìn)行相應(yīng)的改變。上一小節(jié)的響應(yīng)式雛形代碼中只對在setter中承擔(dān)了相應(yīng)過程中的部分功能。而getter則完成了另一部分的功能,即收集視圖層的數(shù)據(jù)依賴。這里的依賴準(zhǔn)確來說應(yīng)該叫做一個觀察者,它負(fù)責(zé)監(jiān)測一對相互關(guān)聯(lián)的數(shù)據(jù)和引用該數(shù)據(jù)的視圖,并維護(hù)著新數(shù)據(jù)渲染的方法。
vue響應(yīng)式系統(tǒng)實現(xiàn)原理到此已經(jīng)大致清晰:為data中所有屬性綁定其存取屬性getter和setter,其中,getter用來收集,setter用來更新。每當(dāng)視圖層對數(shù)據(jù)進(jìn)行讀取,則調(diào)用getter,將對應(yīng)依賴收集起來;每當(dāng)邏輯層改變該數(shù)據(jù),則調(diào)用setter函數(shù),依次更新收集到的所有依賴。
下面再重新審視官網(wǎng)文檔上的這個原理圖就要清楚的多,其中“touch”的過程就是渲染視圖時讀數(shù)據(jù)觸發(fā)getter的過程,而“wathcer”就是上文說的觀察者,它具體是怎樣的實現(xiàn)將在之后的小節(jié)中進(jìn)行具體說明。

2 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)
2.1 訂閱者Dep
訂閱者Dep本質(zhì)是一個類,其功能簡單說就是一個收集管理處。我們知道,對于vue組件實例data中的某一數(shù)據(jù),可能被視圖層多處依賴,每一處依賴,就有一個對應(yīng)的觀察者watcher來負(fù)責(zé)執(zhí)行視圖的變化更新。所以為了在數(shù)據(jù)變化時更新到所有的視圖層數(shù)據(jù),對于每一個數(shù)據(jù),我們都需要維護(hù)這樣一個數(shù)據(jù)結(jié)構(gòu)Dep來收集所有引用該數(shù)據(jù)的watcher,以使得數(shù)據(jù)變化時,它能一一通知收集到的watcher去執(zhí)行對應(yīng)的更新函數(shù)。dep與watcher的關(guān)系如下圖所示:

具體來說,訂閱者對象實例承擔(dān)了以下工作:
- 收集watcher。
- 存儲watcher。
- 數(shù)據(jù)更新時,循環(huán)通知所有watcher更新對應(yīng)視圖。
這里值得提及一下Dep實例收集觀察者的過程,源碼中采取了巧妙的方式使得一個watcher一旦被實例化,便自己將自己加入對應(yīng)的dep中。其具體過程如下:
1). Dep類自身定義了靜態(tài)變量target,指向新new出的watcher。
2).watcher在構(gòu)造函數(shù)中會為了保存當(dāng)前值(以便待觀察數(shù)據(jù)被賦予新值時進(jìn)行比較)而讀取數(shù)據(jù)。
3).觸發(fā)該數(shù)據(jù)的getter,而每個數(shù)據(jù)的getter中會調(diào)用對應(yīng)dep的收集函數(shù)將target所指向的watcher實例存儲起來。
4).解除target指向直到有新的watcher被實例化出來。
基于以上所述,可封裝如下訂閱者對象:
let depId = 0;
class Dep {
constructor() {
this.id = depId++;
//存儲watcher
this.subs = [];
}
//添加watcher
addSub(watcher) {
this.subs.push(watcher);
}
depend(){
Dep.target.addDep(this);
}
//數(shù)據(jù)變化,通知所有觀察者更新對應(yīng)視圖
notify() {
this.subs.forEach(watcher =>{
//依賴更新視圖
watcher.update();
})
}
}
Dep.target = null;
數(shù)據(jù)綁定存取屬性的過程也進(jìn)一步封裝為一個函數(shù),并補(bǔ)充完整其getter的內(nèi)容,這里每個帶觀測數(shù)據(jù)和每個dep實例是一一對應(yīng)的關(guān)系:
function defineReactive (obj,key,value) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
if(Dep.target){
dep.depend();
}
return value;
},
set (newValue) {
if(value !== newValue) {
value = newValue;
dep.notify();
}
}
})
}
function observer(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj,key,obj[key]);
if(typeof obj[key] === 'object') {
observer(obj[key]);
}
})
}
2.2 觀察者watcher
觀察者watcher,其本質(zhì)為一個對象,我們在組件實例中定義的watch的成員就是在為數(shù)據(jù)綁定一個個的watcher,視圖部分有可能是dom中一個元素屬性或文本節(jié)點,不同形式的視圖層其更新方式有所不同。在響應(yīng)式的環(huán)節(jié)中,每個觀察者存有對應(yīng)視圖的更新方法。
由上節(jié)我們知道,當(dāng)一個數(shù)據(jù)在邏輯層發(fā)生改變,會首先通知給watcher的收集管理處Dep,在由Dep一一傳達(dá)收集的watcher,此時每個watcher調(diào)用對應(yīng)的更新方法去更新視圖。具體來說,watcher在這一過程中做了以下工作:
- 首次實例化,將自己注冊在訂閱者Dep中。
- 解析待觀察的表達(dá)式,在data中獲取對應(yīng)的新值,存儲舊值。
- 比較新舊值,當(dāng)新舊值不同,調(diào)用更新方法更新視圖。
基于以上所述, watcher類定義的框架大致如下:
class Watcher {
constructor (vm,expOrFunc,cb) {
//vm vue實例
this.vm = vm;
// 被觀察的屬性變量名稱
this.exp = expOrFunc;
this.getter = function(vm, exp){
return vm.$data[exp];
};
this.id = watcherId++;
//屬性賦新值后調(diào)用回調(diào)
this.cb = cb;
this.deps = [];
this.value = this.get(); //獲取老值
}
get(){
Dep.target = this;
let value = this.getter(this.vm,this.exp);
//配合getter中Dep.target非空判斷防止相同watcher二次加入,讀后需解綁
Dep.target = null;
return value;
}
//注冊
addDep(dep){
if(this.deps.indexOf(dep.id) === -1){
this.deps.push(dep.id);
dep.addSub(this);
}
}
//對外暴露的方法
update(){
let value = this.get(); //新值
if(this.value !== value) {
const oldValue = this.value;
this.value = value;
this.cb.call(this.vm,value,oldValue);
}
}
}
2.3 執(zhí)行
不考慮各種各樣的邊界情況,到這里我們關(guān)鍵數(shù)據(jù)結(jié)構(gòu)已經(jīng)構(gòu)建完全,可以進(jìn)行實例化并簡單的模擬響應(yīng)式數(shù)據(jù)。由于代碼中未加入html模板編譯的過程,這里僅用js定義watch的形式來產(chǎn)生一個watcher觀察數(shù)據(jù), 回調(diào)函數(shù)在控制臺打印更新后的數(shù)據(jù)。具體代碼如下:
class Vue {
constructor(options){
this._data = options.data;
observer(this._data);
if(options.watch) {
Object.keys(this._data).forEach((key)=>{
const watcher = new Watcher(this,key,options.watch[key]);
})
}
}
}
let o = new Vue({
data: {
a: 10,
b: 'hhhh'
},
watch: {
'a': function (newValue) {
console.log("update a:"+newValue)
},
'b': function (newValue) {
console.log("update b:"+newValue)
}
}
})
在vue實例構(gòu)建的時候,會調(diào)用observer函數(shù)對data對象中的每個屬性進(jìn)行響應(yīng)式化,即定義他們的getter和setter并初始化每個屬性對應(yīng)的dep實例。同時根據(jù)配置屬性watch來生成一個針對屬性a的watcher,每當(dāng)這個數(shù)據(jù)發(fā)生變化時,將調(diào)用回調(diào)函數(shù)更新視圖(這里只是控制臺輸出)。說明起見,每個響應(yīng)式屬性在setter中執(zhí)行完屬性收集,將打印一下對應(yīng)的dep.subs。代碼執(zhí)行,控制臺打印如下:

控制臺輸出兩個watcher的數(shù)組,由于數(shù)據(jù)a,b各自僅擁有一個觀察者watcher,因此每個數(shù)組長度均為1,id分別為0和1。屬性deps解釋一下,該屬性維護(hù)了其已注冊了的訂閱者實例dep的id,一旦watcher的注冊函數(shù)addDep被調(diào)用,其首先會從屬性deps中查看其在這個dep中是否已被注冊過,如果是,則不重新注冊。
當(dāng)改變某個響應(yīng)式屬性,會在賦值時在控制臺打印新值:

在控制臺改變一下數(shù)據(jù)a的值,觸發(fā)了a所綁定的setter,從而讓a的訂閱者去
通知其subs中所有的watcher調(diào)用update方法去更新視圖,最終調(diào)用了傳給watcher的回調(diào)函數(shù),在控制臺打印“update a:70”。這里還會打印了dep.subs是因為在真正更新視圖前,需要調(diào)用get函數(shù)去讀取一下新值,所以又觸發(fā)了一次setter。由于我們做了防止watcher重復(fù)注冊的判斷,故打印出的dep.subs中依然只有id為0的一個watcher。
(其實我也疑惑為什么不在setter中直接傳值newValue不就無需觸發(fā)getter了嘛,還有為什么watcher加入dep的行為不直接在dep中push了還兜那么大圈子... 也許是這樣的寫法解耦的比較徹底...)
然而,還有一個問題值得思考,vue中我們觀察的很可能是個對象,比如a.name、a.name.first這樣,當(dāng)對象內(nèi)部的值發(fā)生改變,視圖依然可以發(fā)生改變。做到這樣的深度觀察,即需要為對象內(nèi)部的值也定義好其setter及getter,實現(xiàn)方法不難,無非是遞歸,這里用Array的reduce方法來改變一下watcher中讀取數(shù)據(jù)的方法getter:
this.getter = function(vm, exp){
let exprArr = exp.split('.');
let value = exprArr.reduce((prev,next) => {
return prev[next];
}, vm.$data)
return value;
};
將a的值改為一個數(shù)據(jù)再執(zhí)行下上面的過程: