六千字講透 Vue3 響應式是如何實現的

作者:candyTong

https://juejin.cn/post/7048970987500470279

前言

本文使用 ref 對 vue 的響應性進行解讀,僅僅是響應性原理解析,不涉及 vue 組件等概念。

vue 的響應性的實現,在 @vue/reactivity 包下,對應的源碼目錄爲 packages/reactivity。如何調試 vue 源碼,可查看該文章 [1]

爲什麼使用 ref 進行講解,而不是 reactive?

ref 比 reactive 的實現簡單,且不需要用到 es6 的 Proxy,僅僅需要使用到對象的 getter 和 setter 函數

因此,講述響應性原理,我們用簡單的 ref ,儘量減少大家的理解成本

什麼是響應性?

這部分的響應性定義,來自 vue3 官方文檔 [2]

這個術語在程序設計中經常被提及,但這是什麼意思呢?響應性是一種允許我們以聲明式的方式去適應變化的編程範例。人們通常展示的典型例子,是一份 excel 電子表格 (一個非常好的例子)。

如果將數字 2 放在第一個單元格中,將數字 3 放在第二個單元格中並要求提供 SUM,則電子表格會將其計算出來給你。不要驚奇,同時,如果你更新第一個數字,SUM 也會自動更新。

JavaScript 通常不是這樣工作的——如果我們想用 JavaScript 編寫類似的內容:

let val1 = 2
let val2 = 3
let sum = val1 + val2

console.log(sum) // 5

val1 = 3

console.log(sum) // 仍然是 5
複製代碼

如果我們更新第一個值,sum 不會被修改。

那麼我們如何用 JavaScript 實現這一點呢?

我們這裏直接看 @vue/reactive 的測試用例,來看看怎麼使用,纔會做到響應性的效果

ref 的測試用例

it 包裹的是測試用例的具體內容,我們只需要關注回調裏面的代碼即可。

it('should be reactive'() ={
    const a = ref(1)
    let dummy
    let calls = 0
    effect(() ={
        calls++
        dummy = a.value
    })
    expect(calls).toBe(1)
    expect(dummy).toBe(1)
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
    // same value should not trigger
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
})
複製代碼

我們從測試用例中,可以看出有以下幾點結論:

  1. 被 effect 包裹的函數,會自動執行一次。

  2. 被 effect 函數包裹的函數體,擁有了響應性 —— 當 effect 內的函數中的 ref 對象 a.value 被修改時,該函數會自動重新執行。

  3. 當 a.value 被設置成同一個值時,函數並不會自動的重新執行

effect 是什麼?

官方文檔中的描述 [3]:Vue 通過一個副作用 (effect) 來跟蹤函數。副作用是一個函數的包裹器,在函數被調用之前就啓動跟蹤。Vue 知道哪個副作用在何時運行,並能在需要時再次執行它。

簡單地說,要使一個函數擁有響應性,就應該將它包裹在(傳入)effect 函數里。

那麼這裏也可以稍微猜一下,如果有這麼一個 updateDom 函數:

const a_ref = ref('aaaa')
function updateDom(){
    return document.body.innerText = a_ref.value
}
effect(updateDom)
setTimeout(()=>{
    a_ref.value = 'bbb'
},1000)
複製代碼

只要用 effect 包裹一下,當 a_ref.value 改變,就會自動設置 document.body.innerText,從而更新界面。

(當然這裏也只是猜一下,實際上基本的原理,也與這個差不多,但會複雜很多。由於本文篇幅優先,並沒有涉及到這部分)

依賴收集和觸發更新

要實現響應性,就需要在合適的時機,再次執行副作用 effect。如何確定這個合適的時機?就需要依賴收集(英文術語:track)和觸發更新(英文術語:trigger)

仍然看這個測試用例的例子

it('should be reactive'() ={
    const a = ref(1)
    let dummy
    let calls = 0
    effect(() ={
        calls++
        dummy = a.value
    })
    expect(calls).toBe(1)
    expect(dummy).toBe(1)
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
    // same value should not trigger
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
})
複製代碼

我們已經知道,effect 包裹的函數,要在合適的時機被再次執行,那麼在這個例子中,合適的時機就是,a.value 這個 ref 對象被修改。

由於副作用函數,使用了 a.value,因此副作用函數,依賴 a 這個 ref 變量。我們應該把這個依賴記錄下來。

假如是自己實現,可以這麼寫:

const a = {
    // 當 a 被訪問時,可以將副作用函數存儲在 a 對象的 dependency 屬性中,實際上 @vue/reactivity 會稍微複雜一點 
 get value(){
        const fn = // 假設有辦法拿到 effect 的副作用函數
        // fn 就是以下這個函數
        // () ={
        //    calls++
        //    dummy = a.value
        // })
        a.dependence = fn
    }
    // 當 a.value 被修改時,可以這麼觸發更新
    set value(){
        this.dependence()
    }
}
複製代碼

這樣就可以做到,當 ref 被獲取時,收集依賴(即將副作用函數保存起來);當 ref 被修改時,觸發更新(即調用副作用函數)

當然這個實現非常簡單,實際上還要考慮很多情況,例如:

這些情況都是我們沒有考慮進去的,那麼,接下來,我們就看看真正的 ref 的實現

概念約定

在講解源碼前,我們這裏先對一些概念進行約定:

effect(() ={
    calls++
    dummy = a.value
})
複製代碼

image-20211231112331231

ref 源碼解析

通過 ref 的實現,看依賴是什麼,是怎麼被收集的

ref 對象的實現

export function ref(value?: unknown) {
  return createRef(value)
}

// shallowRef,只是將 createRef 的第二個參數 shallow,標記爲 true
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

function createRef(rawValue: unknown, shallow = false) {
  // 如果已經是ref,則直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}
複製代碼

ref 和 shallowRef, 本質都是 RefImpl 對象實例,只是 shallow 屬性不同

爲了便於理解,我們可以只關注 ref 的實現,即默認 shallow === false

接下來,我們看看 RefImpl 是什麼

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  // 用於存儲依賴的副作用函數
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly _shallow = false) {
    // 保存原始 value 到 _rawValue
    this._rawValue = _shallow ? value : toRaw(value)
    // convert函數的作用是,如果 value 是對象,則使用 reactive(value) 處理,否則返回value
    // 因此,將一個對象傳入 ref,實際上也是調用了 reactive
    this._value = _shallow ? value : convert(value)
  }

  get value() {
    // 收集依賴
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    // 如果值改變,纔會觸發依賴
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // 觸發依賴
      triggerRefValue(this, newVal)
    }
  }
}
複製代碼

在 RefImpl 對象中

因此,只有訪問 / 修改 ref 的 value 屬性,纔會收集 / 觸發依賴

依賴是怎麼被收集的

export function trackRefValue(ref: RefBase<any>) {
  // 判斷是否需要收集依賴
  if (isTracking()) {
    ref = toRaw(ref)
    // 如果沒有 dep 屬性,則初始化 dep,dep 是一個 Set<ReactiveEffect>,存儲副作用函數
    if (!ref.dep) {
      ref.dep = createDep()
    }
    // 收集 effect 依賴
    trackEffects(ref.dep)
  }
}

// 判斷是否需要收集依賴
export function isTracking() {
  // shouldTrack 是一個全局變量,代表當前是否需要 track 收集依賴
  // activeEffect 也是個全局變量,代表當前的副作用對象 ReactiveEffect
  return shouldTrack && activeEffect !== undefined
}
複製代碼

爲什麼需要使用 isTracking,來判斷是否收集依賴?

不是任何情況 ref 被訪問時,都需要收集依賴。例如:

ref.dep 有什麼作用?

ref.dep 的類型是Set<ReactiveEffect> ,關於 ReactiveEffect 的細節會在後面詳細闡述

ref.dep 用於存儲副作用對象,這些副作用對象,依賴該 ref,ref 被修改時就會觸發

我們再來看看 trackEffects:

// 代表當前的副作用 effect
let activeEffect: ReactiveEffect | undefined

export function trackEffects(
  dep: Dep
) {
  // 這個是局部變量的 shouldTrack,跟上一部分的全局 shouldTrack 不一樣
  let shouldTrack = false
  // 已經 track 收集過依賴,就可以跳過了
  shouldTrack = !dep.has(activeEffect!)

  if (shouldTrack) {
    // 收集依賴,將 effect 存儲到 dep
    dep.add(activeEffect!)
    // 同時 effect 也記錄一下 dep
    // 用於 trigger 觸發 effect 後,刪除 dep 裏面對應的 effect,即 dep.delete(activeEffect)
    activeEffect!.deps.push(dep)
  }
}
複製代碼

收集依賴,就是把 activeEffect(當前的副作用對象),保存到 ref.dep 中(當觸發依賴時,遍歷 ref.dep 執行 effect )

然後把 ref.dep,也保存到 effect.deps 中(用於在觸發依賴後, ref.dep.delete(effect),雙向刪除依賴)

image-20211230205303018

依賴是怎麼被觸發的

看完 track 收集依賴,那看看依賴是怎麼被觸發的

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  // ref 可能是 reactive 對象的某個屬性的值
  // 這時候在 triggerRefValue(this, newVal) 時取 this,拿到的是一個 reactive 對象
  // 需要獲取 Proxy 代理背後的真實值 ref 對象
  ref = toRaw(ref)
  // 有依賴才觸發 effect
  if (ref.dep) {
     triggerEffects(ref.dep)
  }
}
複製代碼

再來看看 triggerEffects

export function triggerEffects(
  dep: Dep | ReactiveEffect[]
) {
  // 循環遍歷 dep,去取每個依賴的副作用對象 ReactiveEffect
  for (const effect of isArray(dep) ? dep : [...dep]) {
    // 默認不允許遞歸,即當前 effect 副作用函數,如果遞歸觸發當前 effect,會被忽略
    if (effect !== activeEffect || effect.allowRecurse) {
      // effect.scheduler可以先不管,ref 和 reactive 都沒有
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        // 執行 effect 的副作用函數
        effect.run()
      }
    }
  }
}
複製代碼

這裏省略了一些代碼,這樣結構更清晰。

當 ref 被修改時,會 trigger 觸發依賴,即執行了 ref.dep 裏的所有副作用函數(effect.run 運行副作用函數)

爲什麼默認不允許遞歸?

const foo = ref([])
effect(()=>{
    foo.value.push(1)
})
複製代碼

在這個副作用函數中,即會使用到 foo.value(getter 收集依賴),又會修改 foo 數組(觸發依賴)。如果允許遞歸,會無限循環。

至此,ref 依賴收集和觸發的邏輯,已經比較清晰了。

那麼,接下來,我們需要進一步瞭解的是,effect 函數、ReactiveEffect 副作用對象、副作用函數,它們是什麼,它們之間有什麼關係?

effect 函數

我們來看一下 effect 的實現

// 傳入一個 fn 函數
export function effect<T = any>(
  fn: () => T
){
  // 參數 fn,可能也是一個 effect,所以要獲取到最初始的 fn 參數
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  // 創建 ReactiveEffect 對象
  const _effect = new ReactiveEffect(fn)
  _effect.run()
  
  const runner = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}
複製代碼

effect 函數接受一個函數作爲參數,該函數,我們稱之爲副作用函數

effect 函數內部,會創建 ReactiveEffect 對象,我們稱之爲副作用對象

effect 函數,返回一個 runner,是一個函數,直接調用就是調用副作用函數;runner 的屬性 effect,保存着它對應的 ReactiveEffect 對象 。

因此,它們的關係如下:

effect 函數的入參爲副作用函數,在 effect 函數內部會創建副作用對象

我們繼續深入看看 ReactiveEffect 對象的實現

ReactiveEffect 副作用對象

該部分(effect.run 函數)代碼有比較大的刪減,點擊查看未刪減的源碼 [4]

爲什麼要刪減這部分代碼?

在 vue 3.2 版本以後,effect.run 做了優化,提升性能,其中涉及到位運算。

優化方案在極端的情況下(effect 非常多次嵌套),會降級到原來的老方案(優化前,3.2 版本前的方案)

因此,爲了便於理解,我這裏先介紹優化前的方案,深入瞭解,並闡述該方案的缺點, 以便更好地理解爲什麼需要進行優化。

刪減部分爲優化後的方案,這部分的方案會在下一小節進行介紹。

下面是 ReactiveEffect 代碼解析:

// 全局公用的 effect 棧,由於可以 effect 嵌套,因此需要用棧保存 ReactiveEffect 副作用對象
const effectStack: ReactiveEffect[] = []
export class ReactiveEffect<T = any> {
  active = true
    
  // 存儲 Dep 對象,如上一小節的 ref.dep
  deps: Dep[] = []

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    // 可以暫時不看,與 effectScope API 相關 https://v3.cn.vuejs.org/api/effect-scope.html#effectscope
    // 將當前 ReactiveEffect 副作用對象,記錄到 effectScope 中
    // 當 effectScope.stop() 被調用時,所有的 ReactiveEffect 對象都會被 stop
    recordEffectScope(this, scope)
  }

  run() {
    // 如果當前 ReactiveEffect 副作用對象,已經在棧裏了,就不需要再處理了
    if (!effectStack.includes(this)) {
      try {
        // 保存上一個的 activeEffect,因爲 effect 可以嵌套
        effectStack.push((activeEffect = this))
        // 開啓 shouldTrack 開關,緩存上一個值
        enableTracking()

        // 在該 effect 所在的所有 dep 中,清除 effect,下面會詳細闡述
        cleanupEffect(this)
          
        // 執行副作用函數,執行過程中,又會 track 當前的 effect 進來,依賴重新被收集
        return this.fn()
      } finally {
        // 關閉shouldTrack開關,恢復上一個值
        resetTracking()
        // 恢復上一個的 activeEffect
        effectStack.pop()
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
}

// 允許 track
export function enableTracking() {
  // trackStack 是個全局的棧,由於 effect 可以嵌套,所以是否 track 的標記,也需要用棧保存
  trackStack.push(shouldTrack)
  // 打開全局 shouldTrack 開關
  shouldTrack = true
}

// 重置上一個 track 狀態
export function resetTracking() {
  const last = trackStack.pop()
  // 恢復上一個 track 狀態
  shouldTrack = last === undefined ? true : last
}
複製代碼

爲什麼要用棧保存 effect 和 track 狀態?

因爲 effect 可能會嵌套,需要保存之前的狀態,effect 執行完成後恢復

cleanupEffect 做了什麼?

回顧下圖:

image-20220102234627353

effect.deps,也存儲着響應式變量的 dep(dep 是一個依賴集合, ReactiveEffect 對象的集合),目的是在 effect 執行後,在所有的 dep 中刪除當前執行過的 effect,雙向刪除

刪除代碼如下:

function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      // 從 ref.dep 中刪除 ReactiveEffect
      deps[i].delete(effect)
    }
    // 從 ReactiveEffect.deps 中刪除 dep
    deps.length = 0
  }
}
複製代碼

刪除的 ReactiveEffect 如何被重新收集?

在 cleanupEffect 中,在各個 dep 中,刪除該 ReactiveEffect 對象。

在執行 this.fn() 時,執行副作用函數,副作用函數的執行中,當使用到響應式變量(如 ref.value)時,又會 trackEffect,重新收集依賴

爲什麼要先刪除,再重新收集依賴?

因爲執行前後的依賴可能不一致,考慮一下情況:

const switch = ref(true)
const foo = ref('foo')
effect( () = {
  if(switch.value){
    console.log(foo.value)
  }else{
    console.log('else condition')
  }
})
switch.value = false
複製代碼

當 switch 爲 true 時,triggerEffect,雙向刪除後,執行副作用函數,switch、foo 會重新收集到依賴 effect

當 switch 變成 false 後,triggerEffect,雙向刪除後,執行副作用函數,僅有 switch 能重新收集到依賴 effect

image-20211231110604009

由於 effect 副作用函數執行前後,依賴的響應式變量(這裏是 ref )可能不一致,因此 vue 會先刪除全部依賴,再重新收集

細心的你,可能會發現:自己寫 vue 代碼時,很少會出現前後依賴不一致的情況。那既然這樣,刪除全部依賴這個實現就有優化的空間,能不能只刪除失效的依賴呢

依賴更新算法優化

該優化是 vue 3.2 版本引入的,原因即上一小節所說的,可以只刪除失效的依賴。並且在極端的嵌套深度下,能夠降級到 cleanupEffect 方法,對所有依賴進行刪除。

先想想,假如是自己實現,要怎麼寫好呢?

  1. 不使用 cleanupEffect 刪除所有依賴

  2. 執行副作用函數前,給 ReactiveEffect 依賴的響應式變量,加上 was 的標記(was 是 vue 給的名稱,過去的意思)

  3. 執行 this.fn()track 重新收集依賴時,給 ReactiveEffect 的每個依賴,加上 new 的標記

  4. 最後,對失效(有 was 但是沒有 new)依賴進行刪除

爲什麼是標記在響應式對象,而不是 ReactiveEffect ?

再回顧一下響應式變量和 ReactiveEffect 的關係:

image-20211231112331231

ReactiveEffect 依賴響應式變量(ref),響應式變量(ref)擁有多個 ReactiveEffect 依賴

只刪除失效的依賴。就要確定哪些依賴(響應式變量)需要被刪除(實際上是響應式變量的 dep 被刪除)

因此,需要在響應式變量上做標記,對已經不依賴的響應式變量,將它們的 dep,從 ReactiveEffect.deps 中刪除

如何給響應式變量做標記

實現如下:

export const initDepMarkers = ({ deps }: ReactiveEffect) ={
  if (deps.length) {
    // 循環 deps,對每個 dep 進行標記
    for (let i = 0; i < deps.length; i++) {
      // 標記 dep 爲 was,w 是 was 的意思
      deps[i].w |= trackOpBit
    }
  }
}
複製代碼

這部分代碼其實比較難理解,尤其是使用了位運算符,如果一開始就解析這些代碼的話,很容易就勸退了。

下面我們對問題進行分析:

爲什麼這裏標記的是 dep?

這裏的 dep,對於 ref,就是 ref.dep,它是一個 Set<ReactiveEffect> 。

dep 跟 ref 的關係是一一對應的,一個 ref 僅僅有一個 dep,因此,標記在 dep 和 標記在 ref,是等價的

那爲什麼不在響應式變量上標記呢?

因爲響應式變量的類型有幾種:ref、computed、reactive,它們都使用 dep 對象存儲依賴,對它們都有的 dep 對象進行標記,可以將標記代碼更好的進行複用(否則要判斷不同的類型,執行不同的標記邏輯)。

如果未來新增一種響應式變量,只需要也是用 dep 進行存儲依賴即可

這個按位與位運算的作用是什麼?

先來看看 dep 的真實結構,它其實還有兩個屬性 w 和 n:

export type Dep = Set<ReactiveEffect> & TrackedMarkers
type TrackedMarkers = {
  /**
   * wasTracked,代表副作用函數執行前被 track 過
   */
  w: number
  /**
   * newTracked,代表副作用函數執行後被 track
   */
  n: number
}
複製代碼

那這個 w 和 n 是怎麼做標記的?我們先來看看位運算做了什麼,不瞭解位運算的同學 ,可以先看看這裏的介紹 [5]

dep.w |= trackOpBit // 即 dep.w = dep.w | trackOpBit
複製代碼

image-20220103205852303

將響應式變量標記,就是將對應整數的二進制位,設置成 1

dep.n 的標記方法也是如此。

爲什麼要使用位運算?

  1. 位運算速度快

  2. 只需要使用一個 number 類型的數據,就能存儲不同深度的標記(was / new)

如果不使用位運算,需要實現同樣的標記能力,需要用數組存儲不同深度的標記,數據結構如下:

export type Dep = Set<ReactiveEffect> & TrackedMarkers
type TrackedMarkers = {
  /**
   * wasTrackedList,代表副作用函數執行前被 track 過
   * 設計爲數組,是因爲 effect 可以嵌套,代表響應式變量在所在的 effect 深度(嵌套層級)中是否被 track
   */
  wasTrackedList: boolean[]
  /**
   * newTracked,代表副作用函數執行後被 track
   * 設計爲數組,是因爲 effect 可以嵌套,代表響應式變量在所在的 effect 深度(嵌套層級)中是否被 track
   */
  newTrackedList: boolean[]
}
複製代碼

使用數組存儲標記位,修改處理沒有直接位運算快。由於 vue 每次執行副作用函數(一個頁面有非常多的副作用函數),都需要頻繁進行標記,這開銷也是非常大的。因此,這裏使用了運算符,提升了標記的速度,也節省了運行內存

trackOpBit 是什麼?

trackOpBit 是代表當前操作的位,它是由 effect 嵌套深度決定的。

// 全局變量嵌套深度一開始爲 0 
effectTrackDepth = 0

// 每次執行 effect 副作用函數前,全局變量嵌套深度會自增 1,執行完成 effect 副作用函數後會自減
trackOpBit = 1 << ++effectTrackDepth
複製代碼

當深度爲 1 時,trackOpBit 是 2(二進制:00000010),操作的是第二位,將 dep.w 的第二位變成 1

因此如圖所說,dep.w 的第一位是不使用的

爲什麼最大標記嵌套深度爲 30?

從圖中我們可以看到,深度受存儲類型的位數限制,否則就會溢出

在 JavaScript 內部,數值都是以 64 位浮點數的形式儲存,但是做位運算的時候,是以 32 位帶符號的整數進行運算的,並且返回值也是一個 32 位帶符號的整數

<< 30
// 1073741824<< 31
// -2147483648,溢出
複製代碼

因此,深度最大爲 30,超過 30,則需要降級方案,使用全部清除再全部重新收集依賴的方案

判斷響應式變量是否被標記

export const wasTracked = (dep: Dep)boolean =(dep.w & trackOpBit) > 0

export const newTracked = (dep: Dep)boolean =(dep.n & trackOpBit) > 0
複製代碼

使用 wasTracked 和 newTracked 判斷 dep 是否在當前深度被標記

trackOpBit 是一個全局變量,根據當前深度生成的

image-20220103210036377

如圖,如果需要判斷深度爲 2 時(trackOpBit 第 3 位爲 1),是否被標記,僅當 dep.w 的第 3 位爲 1 時, wasTracked 或 newTracked 纔會返回 true

vue 通過這樣巧妙的位運算,快速算出依賴在當前深度是否被標記

副作用對象的優化實現

// 當前 effect 的嵌套深度,每次執行會 ++effectTrackDepth
let effectTrackDepth = 0
// 最大的 effect 嵌套層數爲 30
const maxMarkerBits = 30      
// 位運算操作的第 trackOpBit 位
export let trackOpBit = 1
export class ReactiveEffect<T = any> {
  run() {
    if (!effectStack.includes(this)) {
      try {
        // 省略代碼: 保存上一個 activeEffect
        
        // trackOpBit: 根據深度生成 trackOpBit
        trackOpBit = 1 << ++effectTrackDepth

        // maxMarkerBits: 可支持的最大嵌套深度,爲 30
        // 這裏就是之前說到的,正常情況下使用優化方案,極端嵌套場景下,使用降級方案
        if (effectTrackDepth <= maxMarkerBits) {
          // 標記所有的 dep 爲 was
          initDepMarkers(this)
        } else {
          // 降級方案,刪除所有的依賴,再重新收集
          cleanupEffect(this)
        }
         // 執行過程中標記新的 dep 爲 new
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          // 對失效依賴進行刪除
          finalizeDepMarkers(this)
        }
  // 恢復上一次的狀態
        // 嵌套深度 effectTrackDepth 自減
        // 重置操作的位數
        trackOpBit = 1 << --effectTrackDepth

        // 省略代碼: 恢復上一個 activeEffect
      }
    }
  }
}
複製代碼

整體的思路如下:

  1. 執行副作用函數前,給 ReactiveEffect 依賴的響應式變量,加上 was 的標記(was 是 vue 給的名稱,表示過去依賴)

  2. 執行 this.fn()track 重新收集依賴時,給 ReactiveEffect 的每個依賴,加上 new 的標記

  3. 對失效依賴進行刪除(有 was 但是沒有 new)

  4. 恢復上一個深度的狀態

  1. 雙向刪除 ReactiveEffect 副作用對象的所有依賴(effect.deps.length = 0)

  2. 執行 this.fn()track 重新收集依賴時

  3. 恢復上一個深度的狀態

標記 ReactiveEffect 的所有的 dep 爲 was 的實現:

export const initDepMarkers = ({ deps }: ReactiveEffect) ={
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // 遍歷每個 dep 標記爲 was
    }
  }
}
複製代碼

對失效依賴進行刪除的實現如下(有 was 但是沒有 new):

export const finalizeDepMarkers = (effect: ReactiveEffect) ={
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i]
      //有 was 標記但是沒有 new 標記,應當刪除
      if (wasTracked(dep) && !newTracked(dep)) {
        dep.delete(effect)
      } else {
        // 需要保留的依賴,放到數據的較前位置,因爲在最後會刪除較後位置的所有依賴
        deps[ptr++] = dep
      }
      // 清理 was 和 new 標記,將它們對應深度的 bit,置爲 0
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    // 刪除依賴,只保留需要的
    deps.length = ptr
  }
}
複製代碼

參考文章

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wqlpHvtz4yrRGwmHme9g4w