深度解析:Vue3如何巧妙的實現強大的computed
- 2020 年 4 月 11 日
- 筆記
前言
Vue中的computed是一個非常強大的功能,在computed函數中訪問到的值改變了後,computed的值也會自動改變。
Vue2中的實現是利用了Watcher
的嵌套收集,渲染watcher
收集到computed watcher
作為依賴,computed watcher
又收集到響應式數據某個屬性
作為依賴,這樣在響應式數據某個屬性
發生改變時,就會按照 響應式屬性
-> computed值更新
-> 視圖渲染
這樣的觸發鏈觸發過去,如果對Vue2中的原理感興趣,可以看我這篇文章的解析:
手把手帶你實現一個最精簡的響應式系統來學習Vue的data、computed、watch源碼
前置知識
閱讀本文需要你先學習Vue3響應式的基本原理,可以先看我的這篇文章,原理和Vue3是一致的: 帶你徹底搞懂Vue3的Proxy響應式原理!TypeScript從零實現基於Proxy的響應式庫。
在你擁有了一些前置知識以後,默認你應該知道的是:
-
effect
其實就是一個依賴收集函數,在它內部訪問了響應式數據,響應式數據就會把這個effect
函數作為依賴收集起來,下次響應式數據改了就觸發它重新執行。 -
reactive
返回的就是個響應式數據,這玩意可以和effect
搭配使用。
舉個簡單的栗子吧:
// 響應式數據 const data = reactive({ count: 0 }) // 依賴收集 effect(() => console.log(data.count)) // 觸發上面的effect重新執行 data.count ++ 複製程式碼
就這個例子來說,data是一個響應式數據。
effect傳入的函數因為內部訪問到它上面的屬性count
了,
所以形成了一個count -> effect
的依賴。
下次count改變了,這個effect就會重新執行,就這麼簡單。
computed
那麼引入本文中的核心概念,computed
來改寫這個例子後呢:
// 1. 響應式數據 const data = reactive({ count: 0 }) // 2. 計算屬性 const plusOne = computed(() => data.count + 1) // 3. 依賴收集 effect(() => console.log(plusOne.value)) // 4. 觸發上面的effect重新執行 data.count ++ 複製程式碼
這樣的例子也能跑通,為什麼data.count
的改變能間接觸發訪問了計算屬性的effect的重新執行呢?
我們來配合單點調試一步步解析。
簡化版源碼
首先看一下簡化版的computed
的程式碼:
export function computed( getter ) { let dirty = true let value: T // 這裡還是利用了effect做依賴收集 const runner = effect(getter, { // 這裡保證初始化的時候不去執行getter lazy: true, computed: true, scheduler: () => { // 在觸發更新時 只是把dirty置為true // 而不去立刻計算值 所以計算屬性有lazy的特性 dirty = true } }) return { get value() { if (dirty) { // 在真正的去獲取計算屬性的value的時候 // 依據dirty的值決定去不去重新執行getter 獲取最新值 value = runner() dirty = false } // 這裡是關鍵 後續講解 trackChildRun(runner) return value }, set value(newValue: T) { setter(newValue) } } } 複製程式碼
可以看到,computed其實也是一個effect
。這裡對閉包進行了巧妙的運用,注釋里的幾個關鍵點決定了計算屬性擁有懶載入
的特徵,你不去讀取value的時候,它是不會去真正的求值的。
前置準備
首先要知道,effect函數會立即開始執行,再執行之前,先把effect自身
變成全局的activeEffect
,以供響應式數據收集依賴。
並且activeEffect
的記錄是用棧的方式,隨著函數的開始執行入棧,隨著函數的執行結束出棧,這樣就可以維護嵌套的effect關係。
先起幾個別名便於講解
// 計算effect computed(() => data.count + 1) // 日誌effect effect(() => console.log(plusOne.value)) 複製程式碼
從依賴關係來看, 日誌effect
讀取了計算effect
計算effect
讀取了響應式屬性count
所以更新的順序也應該是: count改變
-> 計算effect更新
-> 日誌effect更新
那麼這個關係鏈是如何形成的呢
單步解讀
在日誌effect開始執行的時候,
⭐⭐ 此時activeEffect是日誌effect
此時的effectStack是[ 日誌effect ] ⭐⭐
plusOne.value的讀取,觸發了
get value() { if (dirty) { // 在真正的去獲取計算屬性的value的時候 // 依據dirty的值決定去不去重新執行getter 獲取最新值 value = runner() dirty = false } // 這裡是關鍵 後續講解 trackChildRun(runner) return value }, 複製程式碼
首先進入了求值過程:value = runner()
,runner其實就是計算effect
,它是對於用戶傳入的getter函數的包裝,
進入了runner以後
⭐⭐ 此時activeEffect是計算effect
此時的effectStack是[ 日誌effect, 計算effect ] ⭐⭐ runner所包裹的() => data.count + 1
也就是計算effect
會去讀取count
,因為是由effect包裹的函數,所以觸發了響應式數據的get
攔截:
此時count
會收集計算effect
作為自己的依賴。
並且計算effect
會收集count
的依賴集合,保存在自己身上。(通過effect.deps
屬性)
dep.add(activeEffect) activeEffect.deps.push(dep) 複製程式碼
也就是形成了一個雙向收集的關係,
計算effect
存了count
的所有依賴,count
也存了計算effect
的依賴。
然後在runner運行結束後,計算effect
出棧了,此時activeEffect
變成了棧頂的日誌effect
⭐⭐ 此時activeEffect是日誌effect
此時的effectStack是[ 日誌effect ] ⭐⭐
接下來進入關鍵的步驟:trackChildRun
trackChildRun(runner) function trackChildRun(childRunner: ReactiveEffect) { for (let i = 0; i < childRunner.deps.length; i++) { const dep = childRunner.deps[i] dep.add(activeEffect) } } 複製程式碼
這個runner
就是計算effect
,它的deps
上此時掛著count
的依賴集合,
在trackChildRun
中,它把當前的acctiveEffect也就是日誌effect
也加入到了count
的依賴集合中。
此時count
的依賴集合是這樣的:[ 計算effect, 日誌effect ]
這樣下次count
更新的時候,會把兩個effect都重新觸發,而由於觸發的順序是先觸發computed effect
後觸發普通effect
,因此就完成了
- 計算effect的dirty置為true,標誌著下次讀取需要重新求值。
- 日誌effect讀取計算effect的value,獲得最新的值並列印出來。
總結
不得不承認,computed這個強大功能的實現果然少不了內部非常複雜的實現,這個雙向依賴收集的套路相信也會給各位小夥伴帶來很大的啟發。跟著尤大學習,果然有肉吃!
另外由於@vue/reactivity
的框架無關性,我把它整合進了React,做了一個狀態管理庫,可以完整的使用上述的computed
等強大的Vue3能力。
有興趣的小夥伴也可以看一下,star一下!