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函数内部依赖的响应数据。

这两段话提及来确实有点绕,多理解理解就好。但文章到这也差很少结束了,后面我看看,能不能出一张大图,把整套响应式系统涉及的全部相关数据给绘制清楚,方便你们更直观的了解。

其余几篇文章能够戳专栏主页自行查看。下周我再汇总一篇,作个引导,并把这过程当中有变动的代码再调整一下。谢谢您的阅读。

相关文章
相关标签/搜索