Vue中的computed是一個很是強大的功能,在computed函數中訪問到的值改變了後,computed的值也會自動改變。vue
Vue2中的實現是利用了Watcher
的嵌套收集,渲染watcher
收集到computed watcher
做爲依賴,computed watcher
又收集到響應式數據某個屬性
做爲依賴,這樣在響應式數據某個屬性
發生改變時,就會按照 響應式屬性
-> computed值更新
-> 視圖渲染
這樣的觸發鏈觸發過去,若是對Vue2中的原理感興趣,能夠看我這篇文章的解析:react
手把手帶你實現一個最精簡的響應式系統來學習Vue的data、computed、watch源碼git
閱讀本文須要你先學習Vue3響應式的基本原理,能夠先看個人這篇文章,原理和Vue3是一致的: 帶你完全搞懂Vue3的Proxy響應式原理!TypeScript從零實現基於Proxy的響應式庫。github
在你擁有了一些前置知識之後,默認你應該知道的是:api
effect
其實就是一個依賴收集函數,在它內部訪問了響應式數據,響應式數據就會把這個effect
函數做爲依賴收集起來,下次響應式數據改了就觸發它從新執行。閉包
reactive
返回的就是個響應式數據,這玩意能夠和effect
搭配使用。框架
舉個簡單的栗子吧:函數
// 響應式數據
const data = reactive({ count: 0 })
// 依賴收集
effect(() => console.log(data.count))
// 觸發上面的effect從新執行
data.count ++
複製代碼
就這個例子來講,data是一個響應式數據。post
effect傳入的函數由於內部訪問到它上面的屬性count
了,學習
因此造成了一個count -> effect
的依賴。
下次count改變了,這個effect就會從新執行,就這麼簡單。
那麼引入本文中的核心概念,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
},
複製代碼
runner
就是計算effect
,進入了runner之後
⭐⭐
此時activeEffect是計算effect
此時的effectStack是[ 日誌effect, 計算effect ]
⭐⭐
computed(() => data.count + 1)
日誌effect會去讀取count
,觸發了響應式數據的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
,所以就完成了
不得不認可,computed這個強大功能的實現果真少不了內部很是複雜的實現,這個雙向依賴收集的套路相信也會給各位小夥伴帶來很大的啓發。跟着尤大學習,果真有肉吃!
另外因爲@vue/reactivity
的框架無關性,我把它整合進了React,作了一個狀態管理庫,能夠完整的使用上述的computed
等強大的Vue3能力。
有興趣的小夥伴也能夠看一下,star一下!