一張圖理清 Vue 3.0 的響應式系統

本文首發於個人博客:《一張圖理清 Vue 3.0 的響應式系統》javascript

隨着 Vue 3.0 Pre Alpha 版本的公佈,咱們得以一窺其源碼的實現。Vue 最巧妙的特性之一是其響應式系統,而咱們也可以在倉庫的 packages/reactivity 模塊下找到對應的實現。雖然源碼的代碼量很少,網上的分析文章也有一堆,可是要想清晰地理解響應式原理的具體實現過程,仍是挺費腦筋的事情。通過一天的研究和整理,我把其響應式系統的原理總結成了一張圖,而本文也將圍繞這張圖去講述具體的實現過程。html

vue 3 響應式系統原理

文章涉及到的代碼我也已經上傳到倉庫,結合代碼閱讀本文會更爲流暢哦!vue

一個基本的例子

Vue 3.0 的響應式系統是獨立的模塊,能夠徹底脫離 Vue 而使用,因此咱們在 clone 了源碼下來之後,能夠直接在 packages/reactivity 模塊下調試。java

  1. 在項目根目錄運行 yarn dev reactivity,而後進入 packages/reactivity 目錄找到產出的 dist/reactivity.global.js 文件。
  2. 新建一個 index.html,寫入以下代碼:
<script src="./dist/reactivity.global.js"></script>
<script> const { reactive, effect } = VueObserver const origin = { count: 0 } const state = reactive(origin) const fn = () => { const count = state.count console.log(`set count to ${count}`) } effect(fn) </script>
複製代碼
  1. 在瀏覽器打開該文件,於控制檯執行 state.count++,即可看到輸出 set count to 1

在上述的例子中,咱們使用 reactive() 函數把 origin 對象轉化成了 Proxy 對象 state;使用 effect() 函數把 fn() 做爲響應式回調。當 state.count 發生變化時,便觸發了 fn()。接下來咱們將以這個例子結合上文的流程圖,來說解這套響應式系統是怎麼運行的。react

初始化階段

image

在初始化階段,主要作了兩件事。git

  1. origin 對象轉化成響應式的 Proxy 對象 state
  2. 把函數 fn() 做爲一個響應式的 effect 函數。

首先咱們來分析第一件事。github

你們都知道,Vue 3.0 使用了 Proxy 來代替以前的 Object.defineProperty(),改寫了對象的 getter/setter,完成依賴收集和響應觸發。可是在這一階段中,咱們暫時先無論它是如何改寫對象的 getter/setter 的,這個在後續的」依賴收集階段「會詳細說明。爲了簡單起見,咱們能夠把這部分的內容濃縮成一個只有兩行代碼的 reactive() 函數:數組

export function reactive(target) {
  const observed = new Proxy(target, handler)
  return observed
}
複製代碼

完整代碼在 reactive.js。這裏的 handler 就是改造 getter/setter 的關鍵,咱們放到後文講解。瀏覽器

接下來咱們分析第二件事。函數

當一個普通的函數 fn()effect() 包裹以後,就會變成一個響應式的 effect 函數,而 fn() 也會被當即執行一次

因爲在 fn() 裏面有引用到 Proxy 對象的屬性,因此這一步會觸發對象的 getter,從而啓動依賴收集。

除此以外,這個 effect 函數也會被壓入一個名爲」activeReactiveEffectStack「(此處爲 effectStack)的棧中,供後續依賴收集的時候使用。

來看看代碼(完成代碼請看 effect.js):

export function effect (fn) {
  // 構造一個 effect
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  // 當即執行一次
  effect()
  return effect
}

export function run(effect, fn, args) {
  if (effectStack.indexOf(effect) === -1) {
    try {
      // 往池子裏放入當前 effect
      effectStack.push(effect)
      // 當即執行一遍 fn()
      // fn() 執行過程會完成依賴收集,會用到 effect
      return fn(...args)
    } finally {
      // 完成依賴收集後從池子中扔掉這個 effect
      effectStack.pop()
    }
  }
}
複製代碼

至此,初始化階段已經完成。接下來就是整個系統最關鍵的一步——依賴收集階段。

依賴收集階段

image

這個階段的觸發時機,就是在 effect 被當即執行,其內部的 fn() 觸發了 Proxy 對象的 getter 的時候。簡單來講,只要執行到相似 state.count 的語句,就會觸發 state 的 getter。

依賴收集階段最重要的目的,就是創建一份」依賴收集表「,也就是圖示的」targetMap"。targetMap 是一個 WeakMap,其 key 值是~~當前的 Proxy 對象 state~~代理前的對象origin,而 value 則是該對象所對應的 depsMap。

depsMap 是一個 Map,key 值爲觸發 getter 時的屬性值(此處爲 count),而 value 則是觸發過該屬性值所對應的各個 effect。

仍是有點繞?那麼咱們再舉個例子。假設有個 Proxy 對象和 effect 以下:

const state = reactive({
  count: 0,
  age: 18
})

const effect1 = effect(() => {
  console.log('effect1: ' + state.count)
})

const effect2 = effect(() => {
  console.log('effect2: ' + state.age)
})

const effect3 = effect(() => {
  console.log('effect3: ' + state.count, state.age)
})
複製代碼

那麼這裏的 targetMap 應該爲這個樣子:

image

這樣,{ target -> key -> dep } 的對應關係就創建起來了,依賴收集也就完成了。代碼以下:

export function track (target, operationType, key) {
  const effect = effectStack[effectStack.length - 1]
  if (effect) {
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }

    let dep = depsMap.get(key)
    if (dep === void 0) {
      depsMap.set(key, (dep = new Set()))
    }

    if (!dep.has(effect)) {
      dep.add(effect)
    }
  }
}

複製代碼

弄明白依賴收集表 targetMap 是很是重要的,由於這是整個響應式系統核心中的核心。

響應階段

回顧上一章節的例子,咱們獲得了一個 { count: 0, age: 18 } 的 Proxy,並構造了三個 effect。在控制檯上看看效果:

image

效果符合預期,那麼它是怎麼實現的呢?首先來看看這個階段的原理圖:

vue 3 響應式系統原理

當修改對象的某個屬性值的時候,會觸發對應的 setter。

setter 裏面的 trigger() 函數會從依賴收集表裏找到當前屬性對應的各個 dep,而後把它們推入到 effectscomputedEffects(計算屬性) 隊列中,最後經過 scheduleRun() 挨個執行裏面的 effect。

因爲已經創建了依賴收集表,因此要找到屬性所對應的 dep 也就垂手可得了,能夠看看具體的代碼實現

export function trigger (target, operationType, key) {
  // 取得對應的 depsMap
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    return
  }
  // 取得對應的各個 dep
  const effects = new Set()
  if (key !== void 0) {
    const dep = depsMap.get(key)
    dep && dep.forEach(effect => {
      effects.add(effect)
    })
  }
  // 簡化版 scheduleRun,挨個執行 effect
  effects.forEach(effect => {
    effect()
  })
}
複製代碼

這裏的代碼沒有處理諸如數組的 length 被修改的一些特殊狀況,感興趣的讀者能夠查看 vue-next 對應的源碼,或者這篇文章,看看這些狀況都是怎麼處理的。

至此,響應式階段完成。

總結

閱讀源碼的過程充滿了挑戰性,但同時也經常被 Vue 的一些實現思路給驚豔到,收穫良多。本文按照響應式系統的運行過程,劃分了」初始化「,」依賴收集「和」響應式「三個階段,分別闡述了各個階段所作的事情,應該可以較好地幫助讀者理解其核心思路。最後附上文章實例代碼的倉庫地址,有興趣的讀者能夠自行把玩:

tiny-reactive

相關文章
相關標籤/搜索