深度解析:Vue3如何巧妙的實現強大的computed

前言

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

若是對於 effectreactive 等概念還不夠熟悉,這篇文章暫時是沒辦法看的,能夠先看上面那篇文章。api

在你擁有了一些前置知識之後,默認你應該知道的是:markdown

  1. effect其實就是一個依賴收集函數,在它內部訪問了響應式數據,響應式數據就會把這個effect函數做爲依賴收集起來,下次響應式數據改了就觸發它從新執行。閉包

  2. reactive返回的就是個響應式數據,這玩意能夠和effect搭配使用。框架

舉個簡單的栗子吧:函數

// 響應式數據
const data = reactive({ count: 0 });
// 依賴收集
effect(() => console.log(data.count));
// 觸發上面的effect從新執行
data.count++;
複製代碼

就這個例子來講,data 是一個響應式數據。oop

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中,它把當前的 activeEffect 也就是日誌effect也加入到了count的依賴集合中。

此時count的依賴集合是這樣的:[ 計算effect, 日誌effect ]

這樣下次count更新的時候,會把兩個 effect 都從新觸發,而因爲觸發的順序是先觸發computed effect 後觸發普通effect,所以就完成了

  1. 計算 effect 的 dirty 置爲 true,標誌着下次讀取須要從新求值。
  2. 日誌 effect 讀取計算 effect 的 value,得到最新的值並打印出來。

總結

不得不認可,computed 這個強大功能的實現果真少不了內部很是複雜的實現,這個雙向依賴收集的套路相信也會給各位小夥伴帶來很大的啓發。跟着尤大學習,果真有肉吃!

另外因爲@vue/reactivity的框架無關性,我把它整合進了 React,作了一個狀態管理庫,能夠完整的使用上述的computed等強大的 Vue3 能力。

react-composition-api

有興趣的小夥伴也能夠看一下,star 一下!

相關文章
相關標籤/搜索