Vue3.0源码学习——Watch(上)

「这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战」。

前言

Vue3 Watch 文档

watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。watch 与前文 Vue3.0源码学习——Computed 中的 computed 都是Vue响应式系统中的API,都是根据观察的对象响应的做一些事情,本文主要学习一下 watch 的源码与执行的过程。

源码分析

  • 找到 watch 函数定义的位置 \packages\runtime-core\src\apiWatch.tswatch 有多种定义重载,因此 watch 有多种使用方式,观察源可以是 数组,单值,多值,reactive 对象等

图片.png

  • 观察源 source 类型可以是 RefComputedRef 或者是一个函数返回任意类型 T

图片.png

  • watch 最终定义,接受两个必选参数:观察源 source,回调函数 cb,一个可选参数 选项options,返回一个 doWatch 函数的执行结果就是真正 watch 的实现
// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  ...
  return doWatch(source as any, cb, options)
}
复制代码
  • dowatch 的定义,下面看一下主要的执行流程

图片.png

  • 首先会获取当前组件实例,并声明一个 getter 函数
  const instance = currentInstance
  let getter: () => any
复制代码
  • 根据传入 source 的类型,重新定义 getter 函数,可以看到 getter 最终会是一个函数并且返回我们要观察的值
  if (isRef(source)) { 
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    getter = () => source
    deep = true // 如果直接传入 reactive 默认 deep 会递归
  } else if (isArray(source)) {
    isMultiSource = true
    ...
  } else if (isFunction(source)) { // 直接传入一个函数
    if (cb) {
      // getter with cb
      getter = () =>
        // 防止报错,去执行 cb
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect 
      // 没有传参
      ...
    }
  } else {
   ...
  }
复制代码

如果设置了 deep 就会进行递归操作(递归必然导致性能下降,因此平时开发因劲量避免观察复杂类型数据对象)

  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter()) // 递归
  }
复制代码
  • 创建响应式副作用函数,最终会在未来某个时刻当 依赖getter 发生变化时,进行异步的更新操作 job,即将 cb 再执行一遍,具体更新的操作在之前的文章中学习过 Vue3.0源码学习——更新流程分析
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  // 创建一个计划任务
  const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      // 如果设置了cb,则立刻调用副作用函数,
      // 在这里立即获取watch响应式变量的值
      // 因此这是我们观察的响应式数据的最新值
      const newValue = effect.run()
      if (
        ...
            // 比对新老数据不同,即值发生了变化
          : hasChanged(newValue, oldValue)) ||
        ...
      ) {
        // cleanup before running cb again
        // 每次执行cb前清理
        if (cleanup) {
          cleanup()
        }
        // 调用回调函数cb
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      ...
    }
  }

  ...

  let scheduler: EffectScheduler
  // 根据用户传递的watchOptions,决定如何flush回调cb
  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'
    // 默认不传flush
    scheduler = () => {
      if (!instance || instance.isMounted) {
        // 在pre更新队列中排队,即插队到其他更新队列前
        queuePreFlushCb(job)
      } else {
        ...
      }
    }
  }

  // 创建响应式副作用
  const effect = new ReactiveEffect(getter, scheduler)
复制代码
  • 判断是否立即执行 cb
  // 是否在初始化时运行cb
  if (cb) {
    if (immediate) { // 配置了立即执行
      job() 
    } else {
      oldValue = effect.run() // 保存oldValue,等于执行一次getter
    }
  } else if (flush === 'post') {
    ...
  } else {
    effect.run()
  }
复制代码
  • 最终返回一个清理函数,当不需要 watch 时执行这个函数即可
  // 返回清理函数
  return () => {
    effect.stop()
    if (instance && instance.scope) {
      // 从组件实例上清理响应式副作用函数
      remove(instance.scope.effects!, effect)
    }
  }
复制代码

这样就分析了 watch 函数源码执行的流程,为了更清晰的了解这一过程,将在下一篇中通过代码的调试的形式进一步分析。

猜你喜欢

转载自juejin.im/post/7069435712126320654