Vue3源碼--響應(yīng)式原理1(effect)

?最近學(xué)習(xí)了下Vue3的源碼,抽空寫一些自己對3.x源碼的解讀,同時算是學(xué)習(xí)的一個總結(jié)吧,也能加深自己的印象。
?就先從3.x的響應(yīng)式系統(tǒng)說起吧。

回憶

?首先大概回憶一下2.x的響應(yīng)式系統(tǒng),主要由這幾個模塊組成,Observer,Watcher,Dep。
Observer負責(zé)通過defineProperty劫持Data,每個Data都各自在閉包中維護一個Dep的實例,用于收集依賴著它的Watcher。Dep維護一個公共的Target屬性,用于保存當前的需要被收集依賴的Watcher。每次Data被劫持的getter執(zhí)行的時候,如果Dep.Target!==undefine, dep和Watcher實例就互相收集對方~
?2.x的響應(yīng)式系統(tǒng)其實是圍繞著Watcher,也可以說圍繞著watch API的,包括render是一個renderWatcher,computed是通過lazyWatcher實現(xiàn)。這并不是一個好的設(shè)計模式,不符合六個設(shè)計原則的(單一職責(zé)原則,開閉原則)。而響應(yīng)式系統(tǒng)也無法獨立出來。

對比

?那么3.x是怎樣實現(xiàn)這一塊的內(nèi)容的呢。
?首先3.x響應(yīng)式系統(tǒng)相關(guān)的代碼在packages/reactivity/src里。3.x的響應(yīng)式系統(tǒng)的核心由兩個模塊構(gòu)成: effect, reactive。
?reactive模塊的功能比較簡單,就是給數(shù)據(jù)設(shè)置代理,類似于2.x的Observer,不同的點在于是用的Proxy去做代理。
?effect模塊,傳入一個函數(shù),然后讓這個函數(shù)需要被響應(yīng)式數(shù)據(jù)影響,目前具體在3.x中包括,watch API,computed API,還有組件的更新都是依賴effect實現(xiàn)的,但是這個模塊沒有暴露在Vue對象上面。所以說effect模塊是一個偏向于底層只有基礎(chǔ)功能的模塊,相比2.x,這明顯是一個較好的設(shè)計模式。

Effect

?關(guān)于effect模塊,最主要的是里面的effect,track,trigger三個方法。
?effect方法是一個高階函數(shù),或者也可以說是工廠方法,接收一個函數(shù)作為參數(shù),返回一個effect實例方法,它使這個函數(shù)中的響應(yīng)式數(shù)據(jù)可追蹤到這個effect實例,如果有響應(yīng)式數(shù)據(jù)發(fā)生了改變,就會再次執(zhí)行這個effect,可以參照源碼中調(diào)用這個方法的三個地方computed.ts,apiWatch.ts,renderer.ts
?首先來看看track:以下是track方法的主要邏輯以及注釋,track方法按字面的解釋就是追蹤,會在數(shù)據(jù)Proxy的get代理中調(diào)用,track這個數(shù)據(jù)本身。其實簡單說就做了一件事情,把當前的active effect收集到響應(yīng)式數(shù)據(jù)的depsMap里面。
其實并不復(fù)雜,這里和2.x不同的是,2.x是每個數(shù)據(jù)各自都在閉包中維護deps對象,這里是用一個全局的Store去保存響應(yīng)式數(shù)據(jù)影響的effects,實現(xiàn)了模塊的解耦。

// target為傳入的響應(yīng)式數(shù)據(jù)對象,type為操作類型,key為target上被追蹤的key
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果shouldTrack為false 或者 當前沒有活動中的effect,不需要執(zhí)行追蹤的邏輯
  // shouldTrack為依賴追蹤提供一個全局的開關(guān),可以很方便暫停/開啟,比如用于setup以及生命周期執(zhí)行的時候
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 所有響應(yīng)式數(shù)據(jù)都是被封裝的對象,所以用一個Map來保存更方便,Map的key為響應(yīng)式數(shù)據(jù)的對象
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 同樣為每個響應(yīng)式數(shù)據(jù)按key建立一個Set,用來保存target[key]所影響的effects
  let dep = depsMap.get(key)
  if (dep === void 0) {
    // 用一個Set去保存effects,省去了去重的判斷
    depsMap.set(key, (dep = new Set()))
  }
  // 如果target[key]下面沒有當前活動中的effect,就把這個effect加入到這個deps中
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

?看完track方法的邏輯之后,effect方法的主要邏輯其實就呼之欲出了,那就是啟動響應(yīng)式追蹤---設(shè)置shouldTrack為true,設(shè)置activeEffect為當前的effect,然后再調(diào)用傳入的方法并追蹤依賴,最后返回一個封裝后的實例effect方法。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // createReactiveEffect是一個工廠方法,返回一個函數(shù)實例
  const effect = createReactiveEffect(fn, options)
  // 如果不是lazy effect(lazy effect主要用于computed),立即執(zhí)行這個effect
  if (!options.lazy) {
    effect()
  }
  return effect
}

// createReactiveEffect是一個工廠方法,返回一個函數(shù)實例
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  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.options = options
  return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  // 如果effect.active為false,跳過追蹤直接調(diào)用傳入的函數(shù)
  if (!effect.active) {
    return fn(...args)
  }
  if (!effectStack.includes(effect)) {
    // 清除effect中之前記錄的deps
    cleanup(effect)
    try {
      // 設(shè)置shouldTrack為true
      enableTracking()
      // 設(shè)置activeEffect為當前的effect,另外把當前的effect入棧(比如渲染子組件的時候,這個棧就起作用了)
      effectStack.push(effect)
      activeEffect = effect
      // 執(zhí)行傳入effect的函數(shù)
      return fn(...args)
    } finally {
      effectStack.pop()
      // 設(shè)置shouldTrack為上一次的shouldTrack(注:和effect一樣,shouldTrack也有一個棧)
      resetTracking()
      // 設(shè)置activeEffect為上一個activeEffect
      activeEffect = effectStack[effectStack.length - 1]
    }
  }
}

?最后來看一下trigger方法,trigger方法的調(diào)用在Proxy的set代理中,作用就是在修改一個響應(yīng)式數(shù)據(jù)的時候,執(zhí)行這個響應(yīng)式對象的depsMap中所有的effect。

// target為修改的響應(yīng)式數(shù)據(jù)對象,type為操作類型,key為target上具體修改的參數(shù)
// newValue,oldValue, oldTarget都很好理解
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // never been tracked
    return
  }
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 如果操作類型是CLEAR,說明數(shù)據(jù)類型是Map,或者Set(注意,3.x的響應(yīng)式系統(tǒng)是支持Map和Set的)
  // CLEAR操作需要觸發(fā)集合上的所有屬性的effects
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(dep => {
      // addRunners功能其實很簡單,就是區(qū)分這個effect是普通的effect還是一個computed effect
      addRunners(effects, computedRunners, dep)
    })
  // 如果是更改length長度,說明是個數(shù)組,只需要觸發(fā)key在這個新的length之后的數(shù)據(jù)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        addRunners(effects, computedRunners, dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 大部分的情況,觸發(fā)這個key下面的effets
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    if (
      type === TriggerOpTypes.ADD ||
      type === TriggerOpTypes.DELETE ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      // 如果是添加/刪除數(shù)組里的項,或者Set,Map的add,delete,set幾個方法,同時也會改變length或者size,
      // 在Map和Set里面,受size影響的一些方法(比如size,forEach,entries,keys,values),都會把effect收集到ITERATE_KEY里面。
      // 具體可參考packages/reactivity/src/collectionHandler.ts里面的實現(xiàn)
      const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  const run = (effect: ReactiveEffect) => {
    scheduleRun(
      effect,
      target,
      type,
      key,
      __DEV__
        ? {
            newValue,
            oldValue,
            oldTarget
          }
        : undefined
    )
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  // run每個effect
  computedRunners.forEach(run)
  effects.forEach(run)
}

// addRunners功能其實很簡單,就是區(qū)分這個effect是普通的effect還是一個computed effect
// 普通的effect存在effects里面,computed effect存在computedRunners里面
function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
// 省略
}
// 調(diào)度將要執(zhí)行的effect,是否傳入effect.options.scheduler決定了執(zhí)行的方式
// 若沒有傳入,就立即同步執(zhí)行,若有,則執(zhí)行調(diào)度方法,傳入effect
// 3.x中關(guān)于異步調(diào)度方法的實現(xiàn)可以查看packages/runtime-core/src/scheduler.ts中的queueJob方法
function scheduleRun(
  effect: ReactiveEffect,
  target: object,
  type: TriggerOpTypes,
  key: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  if (effect.options.scheduler !== void 0) {
    effect.options.scheduler(effect)
  } else {
    effect()
  }
}

?以上源碼都是基于 vue-next-alpha8 版本。
?effect模塊相關(guān)的內(nèi)容就這些,下一篇是關(guān)于reactive模塊的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容