這道題目是面試中至關高頻的一道題目了,但凡你簡歷上有寫:「熟練使用Vue
並閱讀過其部分源碼」,那麼這道題目十有八九面試官都會去問你。前端
什麼?你簡歷上不寫閱讀過源碼,那面試官也頗有可能會問你是否閱讀過響應式相關的源碼vue
仍是那句歌詞唱的:面試
掙不脫 逃不過
眉頭解不開的結
命中解不開的劫
複製代碼
做爲一個前端的MVVM
框架,Vue
的基本思路和Angular
、React
並沒有二致,其核心就在於: 當數據變化時,自動去刷新頁面DOM
,這使得咱們能從繁瑣的DOM
操做中解放出來,從而專心地去處理業務邏輯。數組
這就是Vue
的數據雙向綁定(又稱響應式原理)。數據雙向綁定是Vue
最獨特的特性之一。此處咱們用官方的一張流程圖來簡要地說明一下Vue
響應式系統的整個流程: markdown
在Vue
中,每一個組件實例都有相應的watcher
實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的setter
被調用時,會通知watcher
從新計算,從而導致它關聯的組件得以更新。框架
這是一個典型的觀察者模式。函數
在 Vue 數據雙向綁定的實現邏輯裏,有這樣三個關鍵角色:ui
Observer
: 它的做用是給對象的屬性添加getter
和setter
,用於依賴收集和派發更新this
Dep
: 用於收集當前響應式對象的依賴關係,每一個響應式對象包括子對象都擁有一個Dep
實例(裏面subs
是Watcher
實例數組),當數據有變動時,會經過dep.notify()
通知各個watcher
。spa
Watcher
: 觀察者對象 , 實例分爲渲染 watcher (render watcher)
,計算屬性 watcher (computed watcher)
,偵聽器 watcher(user watcher)
三種
爲何要單獨拎出來一小節專門來講這個問題呢?由於大部分同窗只是知道:Vue
的響應式原理是經過Object.defineProperty
實現的。被Object.defineProperty
綁定過的對象,會變成「響應式」化。也就是改變這個對象的時候會觸發get
和set
事件。
可是對於裏面具體的對象依賴關係並非很清楚,這樣也就給了面試官一種:你只是背了答案,對於響應式的內部實現細節,你並非很清楚的印象。
關於Watcher 和 Dep 的關係
這個問題,其實剛開始我也不是很清楚,在查閱了相關資料後,才逐漸對裏面的具體實現有了清晰的理解。
剛接觸Dep
這個詞的同窗都會比較懵: Dep
到底是用來作什麼的呢?咱們經過defineReactive
方法將data
中的數據進行響應式後,雖然能夠監聽到數據的變化了,那咱們怎麼處理通知視圖就更新呢?
Dep
就是幫咱們依賴管理
的。
如上圖所示:一個屬性可能有多個依賴,每一個響應式數據都有一個Dep
來管理它的依賴。
上面說了那麼多,下面我總結一下Vue響應式
的核心設計思路:
當建立Vue
實例時,vue
會遍歷data
選項的屬性,利用Object.defineProperty
爲屬性添加getter
和setter
對數據的讀取進行劫持(getter
用來依賴收集,setter
用來派發更新),而且在內部追蹤依賴,在屬性被訪問和修改時通知變化。
每一個組件實例會有相應的watcher
實例,會在組件渲染的過程當中記錄依賴的全部數據屬性(進行依賴收集,還有computed watcher
,user watcher
實例),以後依賴項被改動時,setter
方法會通知依賴與此data
的watcher
實例從新計算(派發更新),從而使它關聯的組件從新渲染。
到這裏,咱們已經瞭解了「套路」,下面讓咱們用僞代碼來實現一下Vue
的響應式吧!
/** * @name Vue數據雙向綁定(響應式系統)的實現原理 */
// observe方法遍歷幷包裝對象屬性
function observe(target) {
// 若target是一個對象,則遍歷它
if (target && typeof target === "Object") {
Object.keys(target).forEach((key) => {
// defineReactive方法會給目標屬性裝上「監聽器」
defineReactive(target, key, target[key]);
});
}
}
// 定義defineReactive方法
function defineReactive(target, key, val) {
const dep = new Dep();
// 屬性值也多是object類型,這種狀況下須要調用observe進行遞歸遍歷
observe(val);
// 爲當前屬性安裝監聽器
Object.defineProperty(target, key, {
// 可枚舉
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 監聽器函數
set: function (value) {
dep.notify();
},
});
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
複製代碼