深度解析: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

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

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

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

舉個簡單的栗子吧:函數

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

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

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
},
複製代碼

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,所以就完成了

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

總結

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

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

react-composition-api

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

相關文章
相關標籤/搜索