vue3響應式系統源碼解析-Effect篇

前言

爲了更好的作解釋,我會調整源碼中的接口、類型、函數等聲明順序,並會增長一些註釋方便閱讀javascript

若是以前的文章都看過的話,咱們應該已經明白是如何劫持數據了。但還有兩個大問題一直沒解決,即具體是如何收集依賴,又是如何觸發監聽函數的。從前文中,咱們大體能猜到:向effect函數傳遞一個原始函數,會建立一個監聽函數,而且會當即執行一次。而第一次執行時,就能經過讀操做中的track收集到依賴,並在寫操做時,經過trigger時再次觸發這個監聽函數。而這些主要方法的內部邏輯就在 effect 文件中。vue

Effect

外部引入

import { OperationTypes } from './operations'
import { Dep, targetMap } from './reactive'
import { EMPTY_OBJ, extend } from '@vue/shared'
複製代碼

effect 文件的外部引入很少,EMPTY_OBJ指代一個空對象{}extend是一個擴展對象的方法,相似 lodash 中的_.extend。而DeptargetMap正是咱們須要在effect中探索的。java

類型與常量

歸功於以前看過單測,因此看這裏的類型,會輕鬆不少。若是您沒有看過,直接看結論也行。react

// 迭代行爲標識符
export const ITERATE_KEY = Symbol('iterate')

// 監聽函數的配置項
export interface ReactiveEffectOptions {
  // 延遲計算,爲true時候,傳入的effect不會當即執行。
  lazy?: boolean
  // 是不是computed數據依賴的監聽函數
  computed?: boolean
  // 調度器函數,接受的入參run便是傳給effect的函數,若是傳了scheduler,則可經過其調用監聽函數。
  scheduler?: (run: Function) => void
  // **僅供調試使用**。在收集依賴(get階段)的過程當中觸發。
  onTrack?: (event: DebuggerEvent) => void
  // **僅供調試使用**。在觸發更新後執行監聽函數以前觸發。
  onTrigger?: (event: DebuggerEvent) => void
  //經過 `stop` 終止監聽函數時觸發的事件。
  onStop?: () => void
}

// 監聽函數的接口
export interface ReactiveEffect<T = any> {
  // 表明這是一個函數類型,不接受入參,返回結果類型爲泛型T
  // T也便是原始函數的返回結果類型
  (): T
  [effectSymbol]: true
  // 暫時未知,猜想是某種開關
  active: boolean
  // 監聽函數的原始函數
  raw: () => T
  // 暫時未知,根據名字來看是存一些依賴
  // 根據類型來看,存放是二維集合數據,一維是數組,二維是ReactiveEffect的Set集合
  deps: Array<Dep> // === Array<Set<ReactiveEffect>>
  // 如下同上述ReactiveEffectOptions
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}

// debugger事件,這個基本不須要解釋
export type DebuggerEvent = {
  effect: ReactiveEffect
  target: object
  type: OperationTypes
  key: any
} & DebuggerEventExtraInfo

// debugger拓展信息
export interface DebuggerEventExtraInfo {
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

// 存放監聽函數的數組
export const effectStack: ReactiveEffect[] = []
複製代碼

基本骨架

// 是不是監聽函數
export function isEffect(fn: any): fn is ReactiveEffect {
  return fn != null && fn._isEffect === true
}
// 生成監聽函數的effect方法
export function effect<T = any>(
  // 原始函數
  fn: () => T,
  // 配置項
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 若是該函數已是監聽函數了,那賦值fn爲該函數的原始函數
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 建立一個監聽函數
  const effect = createReactiveEffect(fn, options)
  // 若是不是延遲執行的話,當即執行一次
  if (!options.lazy) {
    effect()
  }
  // 返回該監聽函數
  return effect
}
// 建立監聽函數的方法
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // 建立監聽函數,經過run來包裹原始函數,作額外操做
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  // 監聽函數標識符
  effect._isEffect = true
  // 依舊不知道作什麼用的開關
  effect.active = true
  // 原始函數
  effect.raw = fn
  // 應該是存什麼依賴的數組
  effect.deps = []
  // 獲取配置數據
  effect.scheduler = options.scheduler
  effect.onTrack = options.onTrack
  effect.onTrigger = options.onTrigger
  effect.onStop = options.onStop
  effect.computed = options.computed
  return effect
}
複製代碼

能夠看到,這兩個方法,其實都沒作什麼關鍵性的邏輯,也都比較易懂。主要是給監聽函數賦一些屬性。核心仍是在那個run方法中,那裏纔是真正的監聽執行邏輯。不過也有一個不易明白之處是這裏:typescript

// 若是該函數已是監聽函數了,那賦值fn爲該函數的原始函數
if (isEffect(fn)) {
  fn = fn.raw
}
複製代碼

這段邏輯表明着,若是傳遞的函數已是監聽函數了,並不會直接返回舊的監聽函數,而是用其原始函數構建一個新的監聽函數,這在咱們的單測篇中略有體現。effect方法永遠都返回一個新函數,不過暫時不知道這樣設計的緣由是什麼。api

繼續看run方法。數組

// 監聽函數執行器
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  // 若是這個active開關是關上的,那就執行原始方法,並返回
  if (!effect.active) {
    return fn(...args)
  }
  // 若是監聽函數棧中並無此監聽函數,則:
  if (!effectStack.includes(effect)) {
    // 還不知道具體作什麼用的清除行爲
    cleanup(effect)
    try {
      // 將本effect推到effect棧中
      effectStack.push(effect)
      // 執行原始函數並返回
      return fn(...args)
    } finally {
      // 執行完之後將effect從棧中推出
      effectStack.pop()
    }
  }
}

// 傳遞一個監聽函數,作某種清除操做
function cleanup(effect: ReactiveEffect) {
  // 獲取本effect的deps,而後循環清除存儲了自身effect的引用
  // 最後將deps置爲空
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}
複製代碼

這個run方法,因爲引入了cleanupeffectStack,又多了一些判斷,有點兒看不明白。數據結構

問題 1:effect.active是什麼個邏輯?app

咱們搜索下修改effect.active的方法,只有一處:async

export function stop(effect: ReactiveEffect) {
  // 若是active爲true,則觸發effect.onStop,而且把active置爲false。
  if (effect.active) {
    cleanup(effect)
    if (effect.onStop) {
      effect.onStop()
    }
    effect.active = false
  }
}
複製代碼

看過單測篇的話,可能記得有stop這個 api,向它傳參監聽函數,可使得這個監聽函數失去響應式邏輯。那active這個邏輯就明白了。

問題 2:既然執行前effectStack.push(effect),執行後effectStack.pop()。那爲何還會存在effectStack.includes(effect)這種狀況呢?

遇到問題不要慌,記住,咱們有單測大法!咱們把這個 if 邏輯去掉,再跑下 effect 的單測,而後就會發現兩個單測拋錯了。

✕ should avoid implicit infinite recursive loops with itself (26ms)

✕ should allow explicitly recursive raw function loops (12ms)

再搜一下單測代碼,咱們就知道啦,原來是爲了不遞歸循環的。好比在監聽函數中,又改變了依賴數據,按正常邏輯是會不斷的觸發監聽函數的。但經過effectStack.includes(effect)這麼一個判斷邏輯,天然而然就避免了遞歸循環。

而後還有個更使人不解的cleanup。不解的核心緣由是不知道這個deps: Array<Set<ReactiveEffect>>是怎麼寫入的。爲何監聽函數內部會存着一堆監聽函數集合。在這裏爲何又要刪除它。咱們先保留疑問,後面會解答。

另外,從effect函數到run函數,咱們能發現一個顯然不合理之處:

// ...監聽函數類型
interface ReactiveEffect<T = any> {
  (): T
  // ...
}
const effect = function reactiveEffect(...args: unknown[]): unknown {
  return run(effect, fn, args)
} as ReactiveEffect
// ...
fn(...args)
複製代碼

能夠看到,新構建的監聽函數reactiveEffect,居然是有傳參的,而原始函數,以及監聽函數的接口類型類型都是() => T,是沒有傳參的...這裏產生了不一致。按道理來講,因爲監聽函數的基本套路仍是自動觸發的,因此應該是沒有參數的。因此此處的args實際上是沒有意義的。真要傳,在 ts 環境下,也會因爲類型不一致而報錯的。

真要想支持傳參的話,爲了保留原始函數類型(目前是 unknow),須要寫很多類型推導。並且還得限制入參函數必須都是可選的,由於傳入的函數會當即執行一次...因此仍是別傳參了...無論怎麼說,這裏感受能夠優化一下。

回過來,如今咱們的最大問題實際上是,這個effect爲何又會被存在本身的deps裏,又是如何被觸發。這其中的邏輯顯然是在以前一直看到的tracktrigger中。

track

看 track 以前,還得先複習一下targetMap,以前咱們大體知道它是這麼一個結構:

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()
複製代碼

打平了看,就是這樣:WeakMap<Target, Map<string | symbol, Set<ReactiveEffect>>>

這是一個三維的數據結構。Target咱們以前就知道了,是被劫持的原始數據。根據咱們現有的知識(以及個人提早告知),咱們能知道。二維KeyToDepMapkey,就是這個原始對象的屬性 key。而Dep就是存放着監聽函數effect的集合。而後再來看track代碼:

// 收集依賴的函數
export function track( // 原始數據 target: object, // 操做行爲 type: OperationTypes, key?: string | symbol ) {
  // 若是shouldTrack開關關閉,或effectStack中不存在監聽函數,則無須要收集
  if (!shouldTrack || effectStack.length === 0) {
    return
  }
  // 獲取effect棧最後一個effect
  const effect = effectStack[effectStack.length - 1]
  // 是迭代操做的話,從新賦值一下key
  if (type === OperationTypes.ITERATE) {
    key = ITERATE_KEY
  }
  // 獲取二維map,不存在的話,則初始化
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 獲取effect集合,無則初始化
  let dep = depsMap.get(key!)
  if (dep === void 0) {
    depsMap.set(key!, (dep = new Set()))
  }
  // 若是集合中,沒有剛剛獲取的最後一個effect,則將其add到集合dep中
  // 並在effect的deps中也push這個effects集合dep
  if (!dep.has(effect)) {
    dep.add(effect)
    effect.deps.push(dep)
    // 開發環境下時,觸發track鉤子函數
    if (__DEV__ && effect.onTrack) {
      effect.onTrack({
        effect,
        target,
        type,
        key
      })
    }
  }
}
複製代碼

唔,有點兒繞。心中有很多疑問,一個個來摸索。

問題 1:爲何從effectStack尾部獲取的effect就是依賴該target的監聽函數。

那是由於這段邏輯:

try {
  // 將本effect推到effect棧中
  effectStack.push(effect)
  // 執行原始函數並返回
  return fn(...args)
} finally {
  // 執行完之後將effect從棧中推出
  effectStack.pop()
}
複製代碼

fn內引用了依賴數據,執行fn觸發這些數據的get,進而走到了track,而此時effectStack堆棧尾部正好是該effect。不過這裏就有一個隱藏的限制,fn,也就是傳給effect的原始函數,內部的依賴邏輯必須是同步的。好比這樣是行不通的:

let dummy
const obj = reactive({ prop: 1 })
effect(() => {
  setTimeout(() => {
    dummy = obj.prop
  }, 1000)
})
obj.prop = 2
複製代碼

obj.prop的變動,並不會讓監聽函數從新執行。fn也不能是一個async函數。

不過,在 vue3 中的watch函數是支持async。這個在此處就不討論了,主要我還沒研究...

問題 2:targetMapeffect的依賴映射究竟是怎麼樣的。

targetMapdepsMap中存了effect的集合dep,而effect中又存了這個dep...乍看有點兒懵,並且爲何要雙向存?

其實剛剛咱們已經看到了一部分緣由,就是在run方法中執行的cleanup。每次 run 以前,會執行它:

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}
複製代碼

仔細閱讀track方法,咱們大體能理清targetMapeffect.deps中存着的數據具體是怎麼樣的了。分兩步解釋:

  1. 對於一個響應式數據,它在targetMap中存着一個Map數據(我稱之爲「響應依賴映射」)。這個響應依賴映射的key是該響應式數據的某個屬性值,value是全部用到這個響應數據屬性值的全部監聽函數,也便是Set集合dep
  2. 而對於一個監聽函數,它會存放着 全部存着它自身的dep

那問題來了,effect爲何要存着這麼個遞歸數據呢?這是由於要經過cleanup方法,在本身被執行前,把本身從響應依賴映射中刪除了。而後執行自身原始函數fn,而後觸發數據的get,而後觸發track,而後又會把本effect添加到相應的Set<ReactiveEffect>中。有點兒神奇啊,每次執行前,把本身從依賴映射中刪除,執行過程當中,又把本身加回去。

對於這種莫名其妙的邏輯,又是使用單測大法的時候了。我把cleanup的邏輯給註釋了,再跑一會單測,而後會發現以下單測掛了:

✕ should not be triggered by mutating a property, which is used in an inactive branch (3ms)

其單測邏輯爲:

it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
  let dummy
  const obj = reactive({ prop: 'value', run: true })

  const conditionalSpy = jest.fn(() => {
    dummy = obj.run ? obj.prop : 'other'
  })
  effect(conditionalSpy)

  expect(dummy).toBe('value')
  expect(conditionalSpy).toHaveBeenCalledTimes(1)
  obj.run = false
  expect(dummy).toBe('other')
  expect(conditionalSpy).toHaveBeenCalledTimes(2)
  obj.prop = 'value2'
  expect(dummy).toBe('other')
  expect(conditionalSpy).toHaveBeenCalledTimes(2)
})
複製代碼

喔~~~這下咱們就明白了,原來是爲了這種帶有分支處理的狀況。由於監聽函數中,可能會因爲 if 等條件判斷語句致使的依賴數據不一樣。因此每次執行函數時,都要從新更新一次依賴。因此纔有了cleanup這個邏輯。

這樣,咱們就基本搞明白track的套路跟tragetMap的邏輯了,而後攻讀trigger

trigger

咱們先大體瞄一眼這個函數。

// 觸發監聽函數的方法
export function trigger( target: object, // 原始數據 type: OperationTypes, // 寫操做類型 key?: unknown, // 屬性key extraInfo?: DebuggerEventExtraInfo // 拓展信息 ) {
  // 獲取原始數據的響應依賴映射,沒有的話,說明沒被監聽,直接返回
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // never been tracked
    return
  }
  // 聲明一個effect集合
  const effects = new Set<ReactiveEffect>()
  // 聲明一個計算屬性集合
  const computedRunners = new Set<ReactiveEffect>()
  // OperationTypes.CLEAR 表明是集合數據的清除方法,會清除集合數據的全部項
  // 若是是清除操做,那就要執行依賴原始數據的全部監聽方法。由於全部項都被清除了。
  // addRunners並未執行監聽函數,而是將其推到一個執行隊列中,待後續執行
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // key不爲void 0,則說明確定是SET | ADD | DELETE這三種操做
    // 而後將依賴這個key的全部監聽函數推到相應隊列中
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // 若是是增長或者刪除數據的行爲,還要再往相應隊列中增長監聽函數
    // also run for iteration key on ADD | DELETE
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      // 若是原始數據是數組,則key爲length,不然爲迭代行爲標識符
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  // 聲明一個run方法
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }

  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  // 大體翻譯一下:計算屬性的getter函數必須先執行,由於正常的監聽函數,可能會依賴於計算屬性數據

  // 運行全部計算數據的監聽方法
  computedRunners.forEach(run)
  // 運行全部尋常的監聽函數
  effects.forEach(run)
}
複製代碼

除了addRunnersscheduleRun是黑盒外,其餘邏輯大體仍是清晰的。惟獨這兒:

if (key !== void 0) {
  addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
  const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
  addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
複製代碼

每次牽扯到數組/集合以及迭代行爲的時候,老是難以理解,核心是由於咱們對這些數據的底層瞭解比較少。在這裏,咱們不解什麼狀況下會走到第二個邏輯,並且這種狀況確定會重複addRunners,這沒關係嗎?沒事,不懂就跑單測,咱們把第二個 if 註釋了,而後跑一下effect單測:

✕ should observe iteration (5ms)

✕ should observe implicit array length changes (1ms)

✕ should observe enumeration (1ms)

發現確實跟註釋所示,迭代器相關的單測出錯了,部分狀況下的監聽函數沒有觸發。若是以前精讀過reactviehandlers,咱們能大體猜測到緣由。具體舉例來講,相似單測中這樣的狀況(稍微修改了下單測,更易理解):

it('should observe iteration', () => {
  let dummy
  const list = reactive<string[]>([])
  effect(() => (dummy = list.join(' ')))

  expect(dummy).toBe('')
  list.push('Hello')
  expect(dummy).toBe('Hello')
})
複製代碼

此處的effect並無用到數組的某個具體下標,handlers中的track實際上是劫持了數組的length屬性(其實還有join方法,但此處無用),並跟蹤它的變化。在這種狀況下,depsMap實際上是lengtheffects的映射關係。(爲何會tracklength可在上篇文章中尋找答案)

而在上篇文章reactive篇中咱們又知道。數組push行爲觸發的 length 變化,是不會再次觸發trigger的...因而在這個單測中,就只會觸發一次key0valueHellotrigger

在這種狀況下,由於key0,因此if(key !== void 0)確實爲真值,但depsMap.get(0)實際上是爲空的。而depsMap.get('length')纔是真的有相應effect,所以必需要有第二個邏輯作補充。

那問題又來了...對於這樣的操做怎麼辦?

it('should observe iteration', () => {
  let dummy
  const list = reactive<number[]>([])
  effect(() => {
    dummy = list.length + list[0] || 0
  })

  expect(dummy).toBe(0)
  list.push(1)
  expect(dummy).toBe(2)
})
複製代碼

這種狀況下,兩個if邏輯都會跑到,而且depsMap.get(key)depsMap.get(iterationKey)都有值。是否是會執行兩次 effect 呢?其實並不會。咱們繼續看addRunnersscheduleRun

// 將effect添加到執行隊列中
function addRunners( effects: Set<ReactiveEffect>, // 監聽函數集合 computedRunners: Set<ReactiveEffect>, // 計算函數集合 effectsToAdd: Set<ReactiveEffect> | undefined // 待添加的監聽函數或計算函數集合 ) {
  // 若是effectsToAdd不存在,啥也不幹
  if (effectsToAdd !== void 0) {
    // 遍歷effectsToAdd
    // 若是是計算函數,則推到computedRunners,不然推到effects
    effectsToAdd.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
}

function scheduleRun( effect: ReactiveEffect, target: object, type: OperationTypes, key: unknown, extraInfo?: DebuggerEventExtraInfo ) {
  // 開發環境,而且配置了onTrigger,則觸發該函數,傳入相應數據
  if (__DEV__ && effect.onTrigger) {
    effect.onTrigger(
      extend(
        {
          effect,
          target,
          key,
          type
        },
        extraInfo
      )
    )
  }
  // 若是配置了自定義的執行器方法,則執行該方法
  // 不然執行effect
  if (effect.scheduler !== void 0) {
    effect.scheduler(effect)
  } else {
    effect()
  }
}
複製代碼

這兩個方法,看名字很厲害的樣子,其實作的事情很簡單,就是把依賴這個響應式數據的全部effects添加到相應的Set集裏。若是是computed的計算方法,就推到computedRunners裏,不然就推正常的effects集合裏。因爲這兩個都是Set集合。

const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
複製代碼

因此,若是重複添加,是會自動去重的。因此上面兩個if邏輯中若是獲取到了相同的監聽函數,也是會自動去重的,並不會被執行屢次。整個effect就是這麼簡單。沒太多花裏胡哨的,run就是了。

另外讀完之後咱們也能知道,computed方法就是一類特殊的,有返回值的effect。那咱們順路看看完。

computed

關於computed我就不事無鉅細的講了,基本你們都明白了,直接貼核心的重點。

// 函數重載
// 入參爲getter函數
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
// 入參爲配置項
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
// 真正的函數實現
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value: T

  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      dirty = true
    }
  })
  return {
    _isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      // When computed effects are accessed in a parent effect, the parent
      // should track all the dependencies the computed property has tracked.
      // This should also apply for chained computed properties.
      trackChildRun(runner)
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  }
}
複製代碼

能夠看到,邏輯仍是挺簡單的。首先,computed的返回值是一個Ref類型數據。每次get值時,若是沒執行過監聽函數,也就是dirty === true時,執行一遍監聽函數。避免重複獲取時,重複執行。

通常來講,向computed傳遞的是一個function類型,只是獲取一個計算類的數據,返回的數據是沒法修改的。但也有例外,若是傳入的是一個配置項,指定了gettersetter方法,那也是容許手動變動computed數據的。

大體邏輯比較簡單,僅有一個trackChildRun須要多理解一下:

function trackChildRun(childRunner: ReactiveEffect) {
  if (effectStack.length === 0) {
    return
  }
  // 獲取父級effect
  const parentRunner = effectStack[effectStack.length - 1]
  // 遍歷子級,也便是本effect,的deps
  for (let i = 0; i < childRunner.deps.length; i++) {
    const dep = childRunner.deps[i]
    // 若是子級的某dep中沒有父級effect,則將父級effect添加本dep中,而後更新父級effect的deps
    if (!dep.has(parentRunner)) {
      dep.add(parentRunner)
      parentRunner.deps.push(dep)
    }
  }
}
複製代碼

單看代碼,想去理解意思實際上是比較繞的。咱們先理解trackChildRun究竟是爲了什麼。一樣的,使用單測,一招鮮吃遍天。註釋掉它再跑下單測:

✕ should trigger effect (3ms)

✕ should work when chained (2ms)

✕ should trigger effect when chained (1ms)

✕ should trigger effect when chained (mixed invocations) (1ms)

✕ should no longer update when stopped (1ms)

再找到相應單測,咱們就瞭解它的用處了,便是爲了讓依賴computedeffect實現監聽邏輯。以單測舉例來講:

it('should trigger effect', () => {
  const value = reactive<{ foo?: number }>({})
  const cValue = computed(() => value.foo)
  let dummy
  effect(() => {
    dummy = cValue.value
  })
  expect(dummy).toBe(undefined)
  value.foo = 1
  expect(dummy).toBe(1)
})
複製代碼

若是咱們沒有trackChildRun的邏輯,當value變動時,cValue的計算函數確實是能執行的。可是cValue讀跟寫並無tracktrigger的邏輯,當cValue變動時,天然也沒法觸發監聽函數。爲了解決這個問題,因而就有了trackChildRun

監聽函數,也就是單測中的() => { dummy = cValue.value },在它第一次執行時,因爲使用到了cValue,進行了一次計算函數調用,進而走到trackChildRun

而此時,這個監聽函數() => { dummy = cValue.value }還未執行完,所以它還在effectStack隊列末尾。將其從末尾將其取出,便是所謂的computed的父級effect

而計算函數自身也是一個effect,以前咱們說過,它的deps存着全部存着它的dep。而這個dep又指向targetMap中的相應數據。因爲都是引用數據,因此只要把父級effect補充到computed.deps,就等同於作到了父級effect依賴於computed函數內部依賴的響應數據。

這兩段話提及來確實有點繞,多理解理解就好。但文章到這也差很少結束了,後面我看看,能不能出一張大圖,把整套響應式系統涉及的全部相關數據給繪製清楚,方便你們更直觀的瞭解。

其餘幾篇文章能夠戳專欄主頁自行查看。下週我再彙總一篇,作個引導,並把這過程當中有變動的代碼再調整一下。謝謝您的閱讀。

相關文章
相關標籤/搜索