本文首發於我的博客:《一張圖理清 Vue 3.0 的響應式系統》
隨着 Vue 3.0 Pre Alpha 版本的公佈,我們得以一窺其源碼的實現。Vue 最巧妙的特性之一是其響應式系統,而我們也能夠在倉庫的 packages/reactivity 模塊下找到對應的實現。雖然源碼的代碼量不多,網上的分析文章也有一堆,但是要想清晰地理解響應式原理的具體實現過程,還是挺費腦筋的事情。經過一天的研究和整理,我把其響應式系統的原理總結成了一張圖,而本文也將圍繞這張圖去講述具體的實現過程。
文章涉及到的代碼我也已經上傳到倉庫,結合代碼閱讀本文會更爲流暢哦!
一個基本的例子
Vue 3.0 的響應式系統是獨立的模塊,可以完全脫離 Vue 而使用,所以我們在 clone 了源碼下來以後,可以直接在 packages/reactivity 模塊下調試。
- 在項目根目錄運行
yarn dev reactivity
,然後進入packages/reactivity
目錄找到產出的dist/reactivity.global.js
文件。 -
新建一個
index.html
,寫入如下代碼:<script src="./dist/reactivity.global.js"></script> <script> const { reactive, effect } = VueObserver const origin = { count: 0 } const state = reactive(origin) const fn = () => { const count = state.count console.log(`set count to ${count}`) } effect(fn) </script>
- 在瀏覽器打開該文件,於控制檯執行
state.count++
,便可看到輸出set count to 1
。
在上述的例子中,我們使用 reactive()
函數把 origin
對象轉化成了 Proxy 對象 state
;使用 effect()
函數把 fn()
作爲響應式回調。當 state.count
發生變化時,便觸發了 fn()
。接下來我們將以這個例子結合上文的流程圖,來講解這套響應式系統是怎麼運行的。
初始化階段
在初始化階段,主要做了兩件事。
- 把
origin
對象轉化成響應式的 Proxy 對象state
。 - 把函數
fn()
作爲一個響應式的 effect 函數。
首先我們來分析第一件事。
大家都知道,Vue 3.0 使用了 Proxy 來代替之前的 Object.defineProperty()
,改寫了對象的 getter/setter,完成依賴收集和響應觸發。但是在這一階段中,我們暫時先不管它是如何改寫對象的 getter/setter 的,這個在後續的”依賴收集階段“會詳細說明。爲了簡單起見,我們可以把這部分的內容濃縮成一個只有兩行代碼的 reactive()
函數:
export function reactive(target) {
const observed = new Proxy(target, handler)
return observed
}
完整代碼在 reactive.js。這裏的 handler
就是改造 getter/setter 的關鍵,我們放到後文講解。
接下來我們分析第二件事。
當一個普通的函數 fn()
被 effect()
包裹之後,就會變成一個響應式的 effect 函數,而 fn()
也會被立即執行一次。
由於在 fn()
裏面有引用到 Proxy 對象的屬性,所以這一步會觸發對象的 getter,從而啓動依賴收集。
除此之外,這個 effect 函數也會被壓入一個名爲”activeReactiveEffectStack“(此處爲 effectStack)的棧中,供後續依賴收集的時候使用。
來看看代碼(完成代碼請看 effect.js):
export function effect (fn) {
// 構造一個 effect
const effect = function effect(...args) {
return run(effect, fn, args)
}
// 立即執行一次
effect()
return effect
}
export function run(effect, fn, args) {
if (effectStack.indexOf(effect) === -1) {
try {
// 往池子裏放入當前 effect
effectStack.push(effect)
// 立即執行一遍 fn()
// fn() 執行過程會完成依賴收集,會用到 effect
return fn(...args)
} finally {
// 完成依賴收集後從池子中扔掉這個 effect
effectStack.pop()
}
}
}
至此,初始化階段已經完成。接下來就是整個系統最關鍵的一步——依賴收集階段。
依賴收集階段
這個階段的觸發時機,就是在 effect 被立即執行,其內部的 fn()
觸發了 Proxy 對象的 getter 的時候。簡單來說,只要執行到類似 state.count
的語句,就會觸發 state 的 getter。
依賴收集階段最重要的目的,就是建立一份”依賴收集表“,也就是圖示的”targetMap"。targetMap 是一個 WeakMap,其 key 值是當前的 Proxy 對象 state
,而 value 則是該對象所對應的 depsMap。
depsMap 是一個 Map,key 值爲觸發 getter 時的屬性值(此處爲 count
),而 value 則是觸發過該屬性值所對應的各個 effect。
還是有點繞?那麼我們再舉個例子。假設有個 Proxy 對象和 effect 如下:
const state = reactive({
count: 0,
age: 18
})
const effect1 = effect(() => {
console.log('effect1: ' + state.count)
})
const effect2 = effect(() => {
console.log('effect2: ' + state.age)
})
const effect3 = effect(() => {
console.log('effect3: ' + state.count, state.age)
})
那麼這裏的 targetMap 應該爲這個樣子:
這樣,{ target -> key -> dep }
的對應關係就建立起來了,依賴收集也就完成了。代碼如下:
export function track (target, operationType, key) {
const effect = effectStack[effectStack.length - 1]
if (effect) {
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (dep === void 0) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(effect)) {
dep.add(effect)
}
}
}
弄明白依賴收集表 targetMap 是非常重要的,因爲這是整個響應式系統核心中的核心。
響應階段
回顧上一章節的例子,我們得到了一個 { count: 0, age: 18 }
的 Proxy,並構造了三個 effect。在控制檯上看看效果:
效果符合預期,那麼它是怎麼實現的呢?首先來看看這個階段的原理圖:
當修改對象的某個屬性值的時候,會觸發對應的 setter。
setter 裏面的 trigger() 函數會從依賴收集表裏找到當前屬性對應的各個 dep,然後把它們推入到 effects
和 computedEffects(計算屬性)
隊列中,最後通過 scheduleRun()
挨個執行裏面的 effect。
由於已經建立了依賴收集表,所以要找到屬性所對應的 dep 也就輕而易舉了,可以看看具體的代碼實現:
export function trigger (target, operationType, key) {
// 取得對應的 depsMap
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
return
}
// 取得對應的各個 dep
const effects = new Set()
if (key !== void 0) {
const dep = depsMap.get(key)
dep && dep.forEach(effect => {
effects.add(effect)
})
}
// 簡化版 scheduleRun,挨個執行 effect
effects.forEach(effect => {
effect()
})
}
這裏的代碼沒有處理諸如數組的 length 被修改的一些特殊情況,感興趣的讀者可以查看 vue-next 對應的源碼,或者這篇文章,看看這些情況都是怎麼處理的。
至此,響應式階段完成。
總結
閱讀源碼的過程充滿了挑戰性,但同時也常常被 Vue 的一些實現思路給驚豔到,收穫良多。本文按照響應式系統的運行過程,劃分了”初始化“,”依賴收集“和”響應式“三個階段,分別闡述了各個階段所做的事情,應該能夠較好地幫助讀者理解其核心思路。最後附上文章實例代碼的倉庫地址,有興趣的讀者可以自行把玩: