這道題目是面試中相當高頻的一道題目了,但凡你簡歷上有寫:“熟練使用Vue并閱讀過其部分源碼”抡锈,那么這道題目十有八九面試官都會去問你疾忍。
什么?你簡歷上不寫閱讀過源碼床三,那面試官也很有可能會問你是否閱讀過響應式相關的源碼
還是那句歌詞唱的:
掙不脫 逃不過
眉頭解不開的結
命中解不開的劫
整體流程
作為一個前端的MVVM
框架一罩,Vue
的基本思路和Angular讥巡、Reac
t并無二致熙涤,其核心就在于: 當數據變化時,自動去刷新頁面DOM
穷躁,這使得我們能從繁瑣的DOM
操作中解放出來四瘫,從而專心地去處理業(yè)務邏輯汉嗽。
這就是Vue
的數據雙向綁定(又稱響應式原理)。數據雙向綁定是Vue最獨特的特性之一找蜜。此處我們用官方的一張流程圖來簡要地說明一下Vue
響應式系統的整個流程:
在
Vue
中饼暑,每個組件實例都有相應的watche
r實例對象,它會在組件渲染的過程中把屬性記錄為依賴洗做,之后當依賴項的sette
r被調用時弓叛,會通知watche
r重新計算,從而致使它關聯的組件得以更新诚纸。
這是一個典型的觀察者模式撰筷。
關鍵角色
在 Vue
數據雙向綁定的實現邏輯里,有這樣三個關鍵角色:
-
Observer
: 它的作用是給對象的屬性添加getter
和setter
畦徘,用于依賴收集和派發(fā)更新 -
Dep
: 用于收集當前響應式對象的依賴關系,每個響應式對象包括子對象都擁有一個Dep
實例(里面subs
是Watcher
實例數組),當數據有變更時,會通過dep.notify()
通知各個watcher
毕籽。 -
Watcher
: 觀察者對象 , 實例分為渲染watcher (render watcher)
,計算屬性watcher (computed watcher)
,偵聽器watcher(user watcher)
三種
Watcher 和 Dep 的關系
為什么要單獨拎出來一小節(jié)專門來說這個問題呢?因為大部分同學只是知道:Vue
的響應式原理是通過Object.defineProperty
實現的井辆。被Object.defineProperty
綁定過的對象关筒,會變成「響應式」化。也就是改變這個對象的時候會觸發(fā)get
和set
事件杯缺。
但是對于里面具體的對象依賴關系并不是很清楚平委,這樣也就給了面試官一種:你只是背了答案,對于響應式的內部實現細節(jié)夺谁,你并不是很清楚的印象廉赔。
關于Watcher
和 Dep
的關系這個問題肉微,其實剛開始我也不是很清楚,在查閱了相關資料后蜡塌,才逐漸對里面的具體實現有了清晰的理解碉纳。
剛接觸
Dep
這個詞的同學都會比較懵: Dep
究竟是用來做什么的呢?我們通過defineReactive
方法將data
中的數據進行響應式后馏艾,雖然可以監(jiān)聽到數據的變化了劳曹,那我們怎么處理通知視圖就更新呢?Dep
就是幫我們依賴管理的琅摩。如上圖所示:一個屬性可能有多個依賴铁孵,每個響應式數據都有一個
Dep
來管理它的依賴。
一段話總結原理
上面說了那么多房资,下面我總結一下Vue
響應式的核心設計思路:
當創(chuàng)建Vue實例時,vue會遍歷data選項的屬性,利用Object.defineProperty
為屬性添加getter
和setter
對數據的讀取進行劫持(getter
用來依賴收集,setter
用來派發(fā)更新),并且在內部追蹤依賴,在屬性被訪問和修改時通知變化蜕劝。
每個組件實例會有相應的watcher
實例,會在組件渲染的過程中記錄依賴的所有數據屬性(進行依賴收集,還有computed watcher,user watcher
實例),之后依賴項被改動時,setter
方法會通知依賴與此data
的watcher實例重新計算(派發(fā)更新),從而使它關聯的組件重新渲染。
到這里轰异,我們已經了解了“套路”岖沛,下面讓我們用偽代碼來實現一下Vue
的響應式吧!
核心實現
/**
* @name Vue數據雙向綁定(響應式系統)的實現原理
*/
// observe方法遍歷并包裝對象屬性
function observe(target) {
// 若target是一個對象搭独,則遍歷它
if (target && typeof target === "Object") {
Object.keys(target).forEach((key) => {
// defineReactive方法會給目標屬性裝上“監(jiān)聽器”
defineReactive(target, key, target[key]);
});
}
}
// 定義defineReactive方法
function defineReactive(target, key, val) {
const dep = new Dep();
// 屬性值也可能是object類型婴削,這種情況下需要調用observe進行遞歸遍歷
observe(val);
// 為當前屬性安裝監(jiān)聽器
Object.defineProperty(target, key, {
// 可枚舉
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 監(jiān)聽器函數
set: function (value) {
dep.notify();
},
});
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}