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
若是對於 effect
,reactive
等概念還不夠熟悉,這篇文章暫時是沒辦法看的,能夠先看上面那篇文章。api
在你擁有了一些前置知識之後,默認你應該知道的是:markdown
effect
其實就是一個依賴收集函數,在它內部訪問了響應式數據,響應式數據就會把這個effect
函數做爲依賴收集起來,下次響應式數據改了就觸發它從新執行。閉包
reactive
返回的就是個響應式數據,這玩意能夠和effect
搭配使用。框架
舉個簡單的栗子吧:函數
// 響應式數據
const data = reactive({ count: 0 });
// 依賴收集
effect(() => console.log(data.count));
// 觸發上面的effect從新執行
data.count++;
複製代碼
就這個例子來講,data 是一個響應式數據。oop
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
},
複製代碼
首先進入了求值過程: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
中,它把當前的 activeEffect 也就是日誌effect
也加入到了count
的依賴集合中。
此時count
的依賴集合是這樣的:[ 計算effect, 日誌effect ]
這樣下次count
更新的時候,會把兩個 effect 都從新觸發,而因爲觸發的順序是先觸發computed effect
後觸發普通effect
,所以就完成了
不得不認可,computed 這個強大功能的實現果真少不了內部很是複雜的實現,這個雙向依賴收集的套路相信也會給各位小夥伴帶來很大的啓發。跟着尤大學習,果真有肉吃!
另外因爲@vue/reactivity
的框架無關性,我把它整合進了 React,作了一個狀態管理庫,能夠完整的使用上述的computed
等強大的 Vue3 能力。
有興趣的小夥伴也能夠看一下,star 一下!