這道題目是面試中相當(dāng)高頻的一道題目了,但凡你簡歷上有寫:“熟練使用Vue并閱讀過其部分源碼”,那么這道題目十有八九面試官都會去問你。
什么?你簡歷上不寫閱讀過源碼,那面試官也很有可能會問你是否閱讀過響應(yīng)式相關(guān)的源碼
還是那句歌詞唱的:
掙不脫 逃不過
眉頭解不開的結(jié)
命中解不開的劫

整體流程
作為一個(gè)前端的MVVM框架,Vue的基本思路和Angular、React并無二致,其核心就在于: 當(dāng)數(shù)據(jù)變化時(shí),自動去刷新頁面DOM,這使得我們能從繁瑣的DOM操作中解放出來,從而專心地去處理業(yè)務(wù)邏輯。
這就是Vue的數(shù)據(jù)雙向綁定(又稱響應(yīng)式原理)。數(shù)據(jù)雙向綁定是Vue最獨(dú)特的特性之一。此處我們用官方的一張流程圖來簡要地說明一下Vue響應(yīng)式系統(tǒng)的整個(gè)流程:

在
Vue中,每個(gè)組件實(shí)例都有相應(yīng)的watcher實(shí)例對象,它會在組件渲染的過程中把屬性記錄為依賴,之后當(dāng)依賴項(xiàng)的setter被調(diào)用時(shí),會通知watcher重新計(jì)算,從而致使它關(guān)聯(lián)的組件得以更新。
這是一個(gè)典型的觀察者模式。
關(guān)鍵角色
在 Vue 數(shù)據(jù)雙向綁定的實(shí)現(xiàn)邏輯里,有這樣三個(gè)關(guān)鍵角色:
-
Observer: 它的作用是給對象的屬性添加getter和setter,用于依賴收集和派發(fā)更新 -
Dep: 用于收集當(dāng)前響應(yīng)式對象的依賴關(guān)系,每個(gè)響應(yīng)式對象包括子對象都擁有一個(gè)Dep實(shí)例(里面subs是Watcher實(shí)例數(shù)組),當(dāng)數(shù)據(jù)有變更時(shí),會通過dep.notify()通知各個(gè)watcher。 -
Watcher: 觀察者對象 , 實(shí)例分為渲染watcher (render watcher),計(jì)算屬性watcher (computed watcher),偵聽器watcher(user watcher)三種
Watcher 和 Dep 的關(guān)系
為什么要單獨(dú)拎出來一小節(jié)專門來說這個(gè)問題呢?因?yàn)榇蟛糠滞瑢W(xué)只是知道:Vue的響應(yīng)式原理是通過Object.defineProperty實(shí)現(xiàn)的。被Object.defineProperty綁定過的對象,會變成「響應(yīng)式」化。也就是改變這個(gè)對象的時(shí)候會觸發(fā)get和set事件。
但是對于里面具體的對象依賴關(guān)系并不是很清楚,這樣也就給了面試官一種:你只是背了答案,對于響應(yīng)式的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),你并不是很清楚的印象。
關(guān)于Watcher 和 Dep 的關(guān)系這個(gè)問題,其實(shí)剛開始我也不是很清楚,在查閱了相關(guān)資料后,才逐漸對里面的具體實(shí)現(xiàn)有了清晰的理解。

剛接觸
Dep這個(gè)詞的同學(xué)都會比較懵: Dep究竟是用來做什么的呢?我們通過defineReactive方法將data中的數(shù)據(jù)進(jìn)行響應(yīng)式后,雖然可以監(jiān)聽到數(shù)據(jù)的變化了,那我們怎么處理通知視圖就更新呢?Dep就是幫我們依賴管理的。如上圖所示:一個(gè)屬性可能有多個(gè)依賴,每個(gè)響應(yīng)式數(shù)據(jù)都有一個(gè)
Dep來管理它的依賴。
一段話總結(jié)原理
上面說了那么多,下面我總結(jié)一下Vue響應(yīng)式的核心設(shè)計(jì)思路:
當(dāng)創(chuàng)建Vue實(shí)例時(shí),vue會遍歷data選項(xiàng)的屬性,利用Object.defineProperty為屬性添加getter和setter對數(shù)據(jù)的讀取進(jìn)行劫持(getter用來依賴收集,setter用來派發(fā)更新),并且在內(nèi)部追蹤依賴,在屬性被訪問和修改時(shí)通知變化。
每個(gè)組件實(shí)例會有相應(yīng)的watcher實(shí)例,會在組件渲染的過程中記錄依賴的所有數(shù)據(jù)屬性(進(jìn)行依賴收集,還有computed watcher,user watcher實(shí)例),之后依賴項(xiàng)被改動時(shí),setter方法會通知依賴與此data的watcher實(shí)例重新計(jì)算(派發(fā)更新),從而使它關(guān)聯(lián)的組件重新渲染。
到這里,我們已經(jīng)了解了“套路”,下面讓我們用偽代碼來實(shí)現(xiàn)一下Vue的響應(yīng)式吧!
核心實(shí)現(xiàn)
/**
* @name Vue數(shù)據(jù)雙向綁定(響應(yīng)式系統(tǒng))的實(shí)現(xiàn)原理
*/
// observe方法遍歷并包裝對象屬性
function observe(target) {
// 若target是一個(gè)對象,則遍歷它
if (target && typeof target === "Object") {
Object.keys(target).forEach((key) => {
// defineReactive方法會給目標(biāo)屬性裝上“監(jiān)聽器”
defineReactive(target, key, target[key]);
});
}
}
// 定義defineReactive方法
function defineReactive(target, key, val) {
const dep = new Dep();
// 屬性值也可能是object類型,這種情況下需要調(diào)用observe進(jìn)行遞歸遍歷
observe(val);
// 為當(dāng)前屬性安裝監(jiān)聽器
Object.defineProperty(target, key, {
// 可枚舉
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 監(jiān)聽器函數(shù)
set: function (value) {
dep.notify();
},
});
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}