Vue3 源码解析系列 - watch(二)

783 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第21天,点击查看活动详情

前言

上一节学了如何使用watch 方法,和一部分源码,这一篇我们再继续深入研究watch的源码。

doWatch

doWatch 函数的代码比较长,我先看其中一部分

const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false

if (isRef(source)) {
  getter = () => source.value
  forceTrigger = isShallow(source)
} else if (isReactive(source)) {
  getter = () => source
  deep = true
} else if (isArray(source)) {
  isMultiSource = true
  forceTrigger = source.some(isReactive)
  getter = () =>
    source.map(s => {
      if (isRef(s)) {
        return s.value
      } else if (isReactive(s)) {
        return traverse(s)
      } else if (isFunction(s)) {
        return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      }
    })
} else if (isFunction(source)) {
  if (cb) {
    // getter with cb
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  } else {
    // no cb -> simple effect
    getter = () => {
      if (instance && instance.isUnmounted) {
        return
      }
      if (cleanup) {
        cleanup()
      }
      return callWithAsyncErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [onCleanup]
      )
    }
  }
} else {
  getter = NOOP
  __DEV__ && warnInvalidSource(source)
}

首先创建了三个变量,getter最终会传给副作用函数,所以getter就是数据更新后触发的方法,forceTrigger 标识是否需要强制更新,isMultiSource 标记传入的是单个数据源还是以数组形式传入的多个数据源。

接下来就是根据不同的情况创建 getter 方法

  • 先判断 source 是否为 ref,是的话 getter 就是一个返回 source.value 值的函数。
  • 再判断 source 是否为 isReactive,是的化 getter 直接返回 source。
  • 如果 source 是数组,说明是多个数据源,置 isMultiSource 为true,并在 getter 里面遍历 source,分别判断里面的值类型,如果是方法就直接调用。
  • 如果 source 是方法,且watch有传回调,在getter 里面就直接调用这个 source,如果没有传回调,那么说明当前用户使用的 api 不是 watch,而是 watchEffect,如果组件实例已经卸载,则不执行,直接返回,否则执行 cleanup 清除依赖,最后执行 source 函数
  • 如果上面的情况都不满足,说明没有 getter 函数,并且开发环境中会报警告。

处理完 getter,接下来会判断 deep 属性是否为 true,如果为 true,将使用 traverse 来包裹 getter 函数,对数据源中的每个属性递归遍历进行监听。

if (cb && deep) {
  const baseGetter = getter
  getter = () => traverse(baseGetter())
}

接下来会定义一个 job 方法,这个函数最终会作为调度器中的回调函数传入

let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  const job: SchedulerJob = () => {
  if (!effect.active) {
    return
  }
  if (cb) {
    // watch(source, cb)
    const newValue = effect.run()
    if (
      deep ||
      forceTrigger ||
      (isMultiSource
        ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as any[])[i])
          )
        : hasChanged(newValue, oldValue))
    ) {
      if (cleanup) {
        cleanup()
      }
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        newValue,
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onCleanup
      ])
      oldValue = newValue
    }
  } else {
    // watchEffect
    effect.run()
  }
}
  • 在这个调度方法中,会判断回调函数是否有传,没有传就说明是 watchEffect,直接运行 effect.run。
  • 如果有回调,会先执行run方法,的到新值 newValue,如果 deep 为 true,或者在 source 里面的旧值 oldValue 和 newValue 有变化,那么就调用回调方法,并把新值和旧值都传过去,这就是为什么我们在回调方法中,能获得旧值的逻辑。

定义好调度方法后,就开始创建调度器和副作用函数。

let scheduler: EffectScheduler
if (flush === 'sync') {
  scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
  // default: 'pre'
  scheduler = () => queuePreFlushCb(job)
}

const effect = new ReactiveEffect(getter, scheduler)

根据 flush 值的不同,调度器也有所不同。

最后会进行初始化的回调方法执行

// initial run
if (cb) {
  if (immediate) {
    job()
  } else {
    oldValue = effect.run()
  }
} else if (flush === 'post') {
  queuePostRenderEffect(
    effect.run.bind(effect),
    instance && instance.suspense
  )
} else {
  effect.run()
}

return () => {
  effect.stop()
  if (instance && instance.scope) {
    remove(instance.scope.effects!, effect)
  }
}

如果有传参数 immediate 并为true,那么一开始就执行一次 job 方法。最后返回一个方法,这个方法里面调用 effect的stop()方法。

总结

这节我们详细解释了 watch 方法的源码,其实还有个 watchEffect 的api使用的也是 doWatch 方法,并再里面进行了独立处理,但是大体逻辑是一样的。