摘自《剖析Vue.js內(nèi)部運(yùn)行機(jī)制》
Vue.js
是一款MVVM
框架婿奔,數(shù)據(jù)模型僅僅是普通的JavaScript
對(duì)象,但是對(duì)這些對(duì)象進(jìn)行操作時(shí),卻能影響對(duì)應(yīng)視圖碗暗,它的核心實(shí)現(xiàn)就是響應(yīng)式系統(tǒng)
。盡管我們?cè)谑褂?code>Vue.js 進(jìn)行開發(fā)時(shí)不會(huì)直接修改響應(yīng)式系統(tǒng)
梢夯,但是理解它的實(shí)現(xiàn)有助于避開一些常見的坑言疗,也有助于在遇見一些琢磨不透的問題時(shí)可以深入其原理來解決它。
Object.defineProperty
首先我們來介紹一下 Object.defineProperty
颂砸,Vue.js
就是基于它實(shí)現(xiàn)響應(yīng)式系統(tǒng)
的噪奄。
/*
obj: 目標(biāo)對(duì)象
prop: 需要操作的目標(biāo)對(duì)象的屬性名
descriptor: 描述符
return value 傳入對(duì)象
*/
Object.defineProperty(obj, prop, descriptor)
descriptor
的一些屬性:
-
enumerable
:屬性是否可枚舉,默認(rèn)false
沾凄。 -
configurable
:屬性是否可以被修改或者刪除梗醇,默認(rèn)false
。 -
get
:獲取屬性的方法撒蟀。 -
set
:設(shè)置屬性的方法叙谨。
實(shí)現(xiàn) observer (可觀察的)
知道了Object.defineProperty
以后,我們來用它使對(duì)象變成可觀察的保屯。
首先我們定義一個(gè)cb
函數(shù)手负,這個(gè)函數(shù)用來模擬視圖更新,調(diào)用它即代表更新視圖姑尺,內(nèi)部可以是一些更新視圖的方法竟终。
function cb (val) {
/* 渲染視圖 */
console.log("視圖更新啦~");
}
然后我們定義一個(gè)defineReactive
,這個(gè)方法通過 Object.defineProperty
來實(shí)現(xiàn)對(duì)對(duì)象的響應(yīng)式
化切蟋,入?yún)⑹且粋€(gè)obj
(需要綁定的對(duì)象)统捶、key
(obj
的某一個(gè)屬性),val
(具體的值)柄粹。經(jīng)過 defineReactive
處理以后喘鸟,我們的obj
的 key
屬性在讀
的時(shí)候會(huì)觸發(fā)reactiveGetter
方法,而在該屬性被寫
的時(shí)候則會(huì)觸發(fā) reactiveSetter
方法驻右。
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, /* 屬性可枚舉 */
configurable: true, /* 屬性可被修改或刪除 */
get: function reactiveGetter () {
return val; /* 實(shí)際上會(huì)依賴收集什黑,下一小節(jié)會(huì)講 */
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
cb(newVal);
}
});
}
當(dāng)然這是不夠的,我們需要在上面再封裝一層 observer
堪夭。這個(gè)函數(shù)傳入一個(gè)value
(需要響應(yīng)式
化的對(duì)象)愕把,通過遍歷所有屬性的方式對(duì)該對(duì)象的每一個(gè)屬性都通過 defineReactive
處理拣凹。
function observer (value) {
if (!value || (typeof value !== 'object')) {
return;
}
Object.keys(value).forEach((key) => {
defineReactive(value, key, value[key]);
});
}
最后,讓我們用observer
來封裝一個(gè)Vue
吧恨豁!
在Vue
的構(gòu)造函數(shù)中嚣镜,對(duì) options
的 data
進(jìn)行處理,這里的 data
想必大家很熟悉圣絮,就是平時(shí)我們?cè)趯?code>Vue 項(xiàng)目時(shí)組件中的data
屬性(實(shí)際上是一個(gè)函數(shù)祈惶,這里當(dāng)作一個(gè)對(duì)象來簡單處理)。
class Vue {
/* Vue 構(gòu)造類 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
這樣我們只要 new
一個(gè)Vue
對(duì)象扮匠,就會(huì)將 data
中的數(shù)據(jù)進(jìn)行響應(yīng)式
化捧请。如果我們對(duì)data
的屬性進(jìn)行下面的操作,就會(huì)觸發(fā) cb
方法更新視圖棒搜。
let o = new Vue({
data: {
test: "I am test."
}
});
o._data.test = "hello,world."; /* 視圖更新啦~ */
至此疹蛉,響應(yīng)式原理已經(jīng)介紹完了,接下來讓我們學(xué)習(xí)響應(yīng)式系統(tǒng)
的另一部分 ——依賴收集
力麸。
響應(yīng)式系統(tǒng)的依賴收集追蹤原理
栗子一:
我們現(xiàn)在有這么一個(gè) Vue
對(duì)象可款。
new Vue({
template:
`<div>
<span>{{text1}}</span>
<span>{{text2}}</span>
<div>`,
data: {
text1: 'text1',
text2: 'text2',
text3: 'text3'
}
});
然后我們做了這么一個(gè)操作。
this.text3 = 'modify text3';
我們修改了 data
中 text3
的數(shù)據(jù)克蚂,但是因?yàn)橐晥D中并不需要用到 text3
闺鲸,所以我們并不需要觸發(fā)上一章所講的cb
函數(shù)來更新視圖,調(diào)用cb
顯然是不正確的埃叭。
栗子二:
let globalObj = {
text1: 'text1'
};
let o1 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
data: globalObj
});
let o2 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
data: globalObj
});
這個(gè)時(shí)候宅广,我們執(zhí)行了如下操作秸侣。
globalObj.text1 = 'hello,text1';
我們應(yīng)該需要通知o1
以及o2
兩個(gè) vm
實(shí)例進(jìn)行視圖的更新逸贾,依賴收集
會(huì)讓text1
這個(gè)數(shù)據(jù)知道:“有兩個(gè)地方依賴我的數(shù)據(jù)师逸,我變化的時(shí)候需要通知它們”。
接下來我們來介紹一下依賴收集
是如何實(shí)現(xiàn)的类早。
訂閱者 Dep
首先我們來實(shí)現(xiàn)一個(gè)訂閱者Dep
媚媒,它的主要作用是用來存放Watcher
觀察者對(duì)象。
class Dep {
constructor () {
/* 用來存放 Watcher 對(duì)象的數(shù)組 */
this.subs = [];
}
/* 在 subs 中添加一個(gè) Watcher 對(duì)象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有 Watcher 對(duì)象更新視圖 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
為了便于理解我們只實(shí)現(xiàn)了添加的部分代碼涩僻,主要是兩件事情:
- 用
addSub
方法可以在目前的Dep
對(duì)象中增加一個(gè)Watcher
的訂閱操作缭召; - 用
notify
方法通知目前Dep
對(duì)象的subs
中的所有Watcher
對(duì)象觸發(fā)更新操作。
觀察者 Watcher
class Watcher {
constructor () {
/* 在 new 一個(gè) Watcher 對(duì)象時(shí)將該對(duì)象賦值給 Dep.target逆日,在 get 中會(huì)用到 */
Dep.target = this;
}
/* 更新視圖的方法 */
update () {
console.log("視圖更新啦~");
}
}
Dep.target = null;
依賴收集
接下來我們修改一下defineReactive
以及Vue
的構(gòu)造函數(shù)恼琼,來完成依賴收集。
我們?cè)陂]包中增加了一個(gè)Dep
類的對(duì)象屏富,用來收集 Watcher
對(duì)象。在對(duì)象被讀
的時(shí)候蛙卤,會(huì)觸發(fā)reactiveGetter
函數(shù)把當(dāng)前的 Watcher
對(duì)象(存放在Dep.target
中)收集到Dep
類中去狠半。之后如果當(dāng)該對(duì)象被寫
的時(shí)候噩死,則會(huì)觸發(fā) reactiveSetter
方法,通知 Dep
類調(diào)用 notify
來觸發(fā)所有Watcher
對(duì)象的 update
方法更新對(duì)應(yīng)視圖神年。
function defineReactive (obj, key, val) {
/* 一個(gè) Dep 類對(duì)象 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/* 將 Dep.target(即當(dāng)前的 Watcher 對(duì)象存入 dep 的 subs 中) */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
/* 在 set 的時(shí)候觸發(fā) dep 的 notify 來通知所有的 Watcher 對(duì)象更新視圖 */
dep.notify();
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一個(gè) Watcher 觀察者對(duì)象已维,這時(shí)候 Dep.target 會(huì)指向這個(gè) Watcher 對(duì)象 */
new Watcher();
/* 在這里模擬 render 的過程,為了觸發(fā) test 屬性的 get 函數(shù) */
console.log('render~', this._data.test);
}
}
總結(jié)
首先在 observer
的過程中會(huì)注冊(cè)get
方法已日,該方法用來進(jìn)行依賴收集
垛耳。在它的閉包中會(huì)有一個(gè)Dep
對(duì)象,這個(gè)對(duì)象用來存放Watcher
對(duì)象的實(shí)例飘千。其實(shí)依賴收集
的過程就是把Watcher
實(shí)例存放到對(duì)應(yīng)的 Dep
對(duì)象中去堂鲜。
get
方法可以讓當(dāng)前的Watcher
對(duì)象(Dep.target
)存放到它的subs
中(addSub
)方法,在數(shù)據(jù)變化時(shí)护奈,set
會(huì)調(diào)用Dep
對(duì)象的 notify
方法通知它內(nèi)部所有的Watcher
對(duì)象進(jìn)行視圖更新缔莲。這是Object.defineProperty
的 set/get
方法處理的事情,那么依賴收集
的前提條件還有兩個(gè):
- 觸發(fā)
get
方法霉旗; - 新建一個(gè)
Watcher
對(duì)象痴奏。
這個(gè)我們?cè)?code>Vue 的構(gòu)造類中處理。新建一個(gè)
Watcher
對(duì)象只需要new
出來厌秒,這時(shí)候Dep.target
已經(jīng)指向了這個(gè)new
出來的Watcher
對(duì)象來读拆。
而觸發(fā) get
方法也很簡單,實(shí)際上只要把 render function
進(jìn)行渲染鸵闪,那么其中的依賴的對(duì)象都會(huì)被讀取
檐晕,這里我們通過打印來模擬這個(gè)過程,讀取 test
來觸發(fā)get
進(jìn)行依賴收集
岛马。