對于VUE热幔,最顯著的特點(diǎn)之一就是其數(shù)據(jù)雙向綁定而帶來的奇妙開發(fā)體驗(yàn)乐设。經(jīng)由vue源碼中的某些操作,使得工程師在項(xiàng)目開發(fā)過程中绎巨,無需操作Dom近尚,邏輯層對數(shù)據(jù)的改變便會自動反饋在視圖層;反過來场勤,v-model的使用也會使得用戶在視圖層上的修改映射到真實(shí)數(shù)據(jù)上戈锻。
vue官方文檔中有一目---”深入響應(yīng)式原理“,專門闡述了這一特性的實(shí)現(xiàn)機(jī)制和媳,然而篇幅有限格遭,有些具體點(diǎn)的闡述對初學(xué)者來講還是不是很友好。前一段時(shí)間自己專門去找了一些源碼相關(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)聽到一個(gè)數(shù)據(jù)的變化的硬梁,了解這一點(diǎn)是理解”響應(yīng)式“機(jī)制的第一步前硫。
實(shí)現(xiàn)這一功能的是Object.defineProperty。該方法本身的目的在于定義或修改一個(gè)對象的現(xiàn)有屬性荧止,該方法第三個(gè)參數(shù)屬性描述符可通過一對函數(shù)getter和setter來定義一個(gè)屬性的存取特性屹电,它們分別在該屬性被讀取或重新賦值的時(shí)候被調(diào)用。現(xiàn)在可明確跃巡,js即是通過定義待觀測屬性的getter和setter來達(dá)到監(jiān)測其變化危号,進(jìn)而響應(yīng)變化的目的。
到此素邪,可以寫出如下實(shí)現(xiàn)響應(yīng)式系統(tǒng)的雛形外莲,假設(shè)我們要監(jiān)測一個(gè)對象中屬性,當(dāng)其發(fā)生改變時(shí)娘香,自動在控制臺輸出:
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)對視圖層的每一個(gè)值進(jìn)行相應(yīng)的改變淋昭。上一小節(jié)的響應(yīng)式雛形代碼中只對在setter中承擔(dān)了相應(yīng)過程中的部分功能俐填。而getter則完成了另一部分的功能,即收集視圖層的數(shù)據(jù)依賴翔忽。這里的依賴準(zhǔn)確來說應(yīng)該叫做一個(gè)觀察者英融,它負(fù)責(zé)監(jiān)測一對相互關(guān)聯(lián)的數(shù)據(jù)和引用該數(shù)據(jù)的視圖,并維護(hù)著新數(shù)據(jù)渲染的方法歇式。
vue響應(yīng)式系統(tǒng)實(shí)現(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)文檔上的這個(gè)原理圖就要清楚的多,其中“touch”的過程就是渲染視圖時(shí)讀數(shù)據(jù)觸發(fā)getter的過程秸弛,而“wathcer”就是上文說的觀察者铭若,它具體是怎樣的實(shí)現(xiàn)將在之后的小節(jié)中進(jìn)行具體說明。
2 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)
2.1 訂閱者Dep
訂閱者Dep本質(zhì)是一個(gè)類递览,其功能簡單說就是一個(gè)收集管理處叼屠。我們知道,對于vue組件實(shí)例data中的某一數(shù)據(jù)非迹,可能被視圖層多處依賴环鲤,每一處依賴纯趋,就有一個(gè)對應(yīng)的觀察者watcher來負(fù)責(zé)執(zhí)行視圖的變化更新憎兽。所以為了在數(shù)據(jù)變化時(shí)更新到所有的視圖層數(shù)據(jù),對于每一個(gè)數(shù)據(jù)吵冒,我們都需要維護(hù)這樣一個(gè)數(shù)據(jù)結(jié)構(gòu)Dep來收集所有引用該數(shù)據(jù)的watcher纯命,以使得數(shù)據(jù)變化時(shí),它能一一通知收集到的watcher去執(zhí)行對應(yīng)的更新函數(shù)痹栖。dep與watcher的關(guān)系如下圖所示:
具體來說亿汞,訂閱者對象實(shí)例承擔(dān)了以下工作:
- 收集watcher。
- 存儲watcher揪阿。
- 數(shù)據(jù)更新時(shí)疗我,循環(huán)通知所有watcher更新對應(yīng)視圖咆畏。
這里值得提及一下Dep實(shí)例收集觀察者的過程,源碼中采取了巧妙的方式使得一個(gè)watcher一旦被實(shí)例化吴裤,便自己將自己加入對應(yīng)的dep中旧找。其具體過程如下:
1). Dep類自身定義了靜態(tài)變量target,指向新new出的watcher麦牺。
2).watcher在構(gòu)造函數(shù)中會為了保存當(dāng)前值(以便待觀察數(shù)據(jù)被賦予新值時(shí)進(jìn)行比較)而讀取數(shù)據(jù)钮蛛。
3).觸發(fā)該數(shù)據(jù)的getter,而每個(gè)數(shù)據(jù)的getter中會調(diào)用對應(yīng)dep的收集函數(shù)將target所指向的watcher實(shí)例存儲起來剖膳。
4).解除target指向直到有新的watcher被實(shí)例化出來魏颓。
基于以上所述,可封裝如下訂閱者對象:
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)一步封裝為一個(gè)函數(shù)甸饱,并補(bǔ)充完整其getter的內(nèi)容,這里每個(gè)帶觀測數(shù)據(jù)和每個(gè)dep實(shí)例是一一對應(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ì)為一個(gè)對象柜候,我們在組件實(shí)例中定義的watch的成員就是在為數(shù)據(jù)綁定一個(gè)個(gè)的watcher,視圖部分有可能是dom中一個(gè)元素屬性或文本節(jié)點(diǎn)躏精,不同形式的視圖層其更新方式有所不同渣刷。在響應(yīng)式的環(huán)節(jié)中,每個(gè)觀察者存有對應(yīng)視圖的更新方法矗烛。
由上節(jié)我們知道辅柴,當(dāng)一個(gè)數(shù)據(jù)在邏輯層發(fā)生改變,會首先通知給watcher的收集管理處Dep瞭吃,在由Dep一一傳達(dá)收集的watcher碌嘀,此時(shí)每個(gè)watcher調(diào)用對應(yīng)的更新方法去更新視圖。具體來說歪架,watcher在這一過程中做了以下工作:
- 首次實(shí)例化股冗,將自己注冊在訂閱者Dep中。
- 解析待觀察的表達(dá)式和蚪,在data中獲取對應(yīng)的新值止状,存儲舊值。
- 比較新舊值攒霹,當(dāng)新舊值不同怯疤,調(diào)用更新方法更新視圖。
基于以上所述, watcher類定義的框架大致如下:
class Watcher {
constructor (vm,expOrFunc,cb) {
//vm vue實(shí)例
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)行實(shí)例化并簡單的模擬響應(yīng)式數(shù)據(jù)。由于代碼中未加入html模板編譯的過程塔淤,這里僅用js定義watch的形式來產(chǎn)生一個(gè)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實(shí)例構(gòu)建的時(shí)候,會調(diào)用observer函數(shù)對data對象中的每個(gè)屬性進(jìn)行響應(yīng)式化高蜂,即定義他們的getter和setter并初始化每個(gè)屬性對應(yīng)的dep實(shí)例第焰。同時(shí)根據(jù)配置屬性watch來生成一個(gè)針對屬性a的watcher,每當(dāng)這個(gè)數(shù)據(jù)發(fā)生變化時(shí)妨马,將調(diào)用回調(diào)函數(shù)更新視圖(這里只是控制臺輸出)挺举。說明起見,每個(gè)響應(yīng)式屬性在setter中執(zhí)行完屬性收集烘跺,將打印一下對應(yīng)的dep.subs湘纵。代碼執(zhí)行,控制臺打印如下:
控制臺輸出兩個(gè)watcher的數(shù)組滤淳,由于數(shù)據(jù)a,b各自僅擁有一個(gè)觀察者watcher梧喷,因此每個(gè)數(shù)組長度均為1,id分別為0和1脖咐。屬性deps解釋一下铺敌,該屬性維護(hù)了其已注冊了的訂閱者實(shí)例dep的id,一旦watcher的注冊函數(shù)addDep被調(diào)用屁擅,其首先會從屬性deps中查看其在這個(gè)dep中是否已被注冊過偿凭,如果是,則不重新注冊派歌。
當(dāng)改變某個(gè)響應(yīng)式屬性弯囊,會在賦值時(shí)在控制臺打印新值:
在控制臺改變一下數(shù)據(jù)a的值,觸發(fā)了a所綁定的setter胶果,從而讓a的訂閱者去
通知其subs中所有的watcher調(diào)用update方法去更新視圖匾嘱,最終調(diào)用了傳給watcher的回調(diào)函數(shù),在控制臺打印“update a:70”早抠。這里還會打印了dep.subs是因?yàn)樵谡嬲乱晥D前霎烙,需要調(diào)用get函數(shù)去讀取一下新值,所以又觸發(fā)了一次setter蕊连。由于我們做了防止watcher重復(fù)注冊的判斷悬垃,故打印出的dep.subs中依然只有id為0的一個(gè)watcher。
(其實(shí)我也疑惑為什么不在setter中直接傳值newValue不就無需觸發(fā)getter了嘛咪奖,還有為什么watcher加入dep的行為不直接在dep中push了還兜那么大圈子... 也許是這樣的寫法解耦的比較徹底...)
然而盗忱,還有一個(gè)問題值得思考酱床,vue中我們觀察的很可能是個(gè)對象羊赵,比如a.name、a.name.first這樣,當(dāng)對象內(nèi)部的值發(fā)生改變昧捷,視圖依然可以發(fā)生改變闲昭。做到這樣的深度觀察,即需要為對象內(nèi)部的值也定義好其setter及getter靡挥,實(shí)現(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的值改為一個(gè)數(shù)據(jù)再執(zhí)行下上面的過程: