从源码看vue3的watchEffect为什么不应该传入异步函数

1,258 阅读7分钟

出现问题的demo

最近看了一个渡一的视频,讲了一个在watchEffect中使用async函数出现的问题www.douyin.com/video/73275… 它的业务场景是调接口获取的视频地址并播放,同时可以手动修改播放速度倍率,这其实很简单,视频里的代码我敲了一下,确实有问题,当你点击修改倍率的时候,视频播放速度并没有变化,相当于rate的没有被追踪到!

借着这个问题,我们来一起看看watchEffect的源码,来找到问题之所在。

阅读vue3源码

watchEffect

全局搜索一下,可以定位到,watchEffect函数位于packages/runtime-core/src/apiWatch.ts这个文件里,它的代码也很简单。

// Simple effect.
export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
  // 如果是watch
  // return doWatch(source as any, cb, options)
}

可以看出来与watch相比,也只是第二个参数的不同,因此可以说doWatch是这个整个watch的核心代码。

doWatch

// 支持监听reactive,数组,函数(仅watchEffect)
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object, 
  cb: WatchCallback | null, //watch时的callback,watchEffect时为null
  {
    immediate,
    deep,
    flush,
    once,
    onTrack,
    onTrigger,
  }: WatchOptions = EMPTY_OBJ, // watch的第三个参数
): WatchStopHandle {
   // !cb时是watchEffect,反之是watch,后面会多次判断
  // 当watch 且带有once参数,即监听一次后马上停止监听
  if (cb && once) {
    const _cb = cb
    cb = (...args) => {
      _cb(...args)
      unwatch()
    }
  }

  // deep参数将在后续支持传入数字,盲猜是监听到对象第几层
  if (__DEV__ && deep !== void 0 && typeof deep === 'number') {
    warn(
      `watch() "deep" option with number value will be used as watch depth in future versions. ` +
        `Please use a boolean instead to avoid potential breakage.`,
    )
  }
  
  // 如果是watchEffect,不支持一些参数
  if (__DEV__ && !cb) {
    if (immediate !== undefined) {
      warn(
        `watch() "immediate" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`,
      )
    }
    if (deep !== undefined) {
      warn(
        `watch() "deep" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`,
      )
    }
    if (once !== undefined) {
      warn(
        `watch() "once" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`,
      )
    }
  }
  
  // 这个函数用于提示监听到对象不正确
  const warnInvalidSource = (s: unknown) => {
    warn(
      `Invalid watch source: `,
      s,
      `A watch source can only be a getter/effect function, a ref, ` +
        `a reactive object, or an array of these types.`,
    )
  }
  // 当前组件对象,可能为null
  const instance = currentInstance
  // 递归获取reactive对象的keyName,用以实现深度监听
  const reactiveGetter = (source: object) =>
    deep === true
      ? source // traverse will happen in wrapped getter below
      : // for deep: false, only traverse root-level properties
        traverse(source, deep === false ? 1 : undefined)

  let getter: () => any // 获取监听源的函数
  let forceTrigger = false // 是否强制执行一次getter
  let isMultiSource = false // 是否是复合监听源

  if (isRef(source)) { // 如果监听的是ref,watch(ref,cb)
    getter = () => source.value
    forceTrigger = isShallow(source) 
  } else if (isReactive(source)) { // 如果监听的是reactive,watch(reactive,cb)
    getter = () => reactiveGetter(source) //递归获取监听的属性
    forceTrigger = true 
  } else if (isArray(source)) { // 如果监听的是数组,watch([a,b],cb)
    isMultiSource = true // 复合监听源
     // 判断里面有没有reactive 
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () => //把数据源转化为数据值的getter
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return reactiveGetter(s)
        } else if (isFunction(s)) { //如果是函数就应该报错,函数不能也不需要被监听
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s) //如果这些都不是,那就直接报错
        }
      })
  } else if (isFunction(source)) { //如果数据源是单一的function
    if (cb) { // 如果是watch(fun,cn),报错
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
    
      // watchEffect时
      getter = () => {
        if (cleanup) { //如果有副作用函数,先清理副作用
          cleanup()
        }
        //执行effect,这里封装了执行和拦截报错
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup],
        )
      }
    }
  } else {
      // 错误情况
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

  // 2.x array mutation watch compat
  // TODO:还没搞懂__COMPAT__是什么,有没有懂的给我留个言
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter() //拷贝了一个监听源
      if ( // 如果是数组就深度遍历
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }
    // 如果是watch,拷贝一遍监听源,并遍历key
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
  
  let cleanup: (() => void) | undefined
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
      cleanup = effect.onStop = undefined
    }
  }

  // in SSR there is no need to setup an actual effect, and it should be noop
  // 在ssr,我们不能在setup里立即执行监听回调,因为有可能还在node环境里
  // unless it's eager or sync flush 除非是立即执行的回调
  let ssrCleanup: (() => void)[] | undefined
  if (__SSR__ && isInSSRComponentSetup) {
    // we will also not call the invalidate callback (+ runner is not set up)
    onCleanup = NOOP
    if (!cb) {
      getter()
    } else if (immediate) {
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        getter(),
        isMultiSource ? [] : undefined,
        onCleanup,
      ])
    }
    if (flush === 'sync') {
    // ssr时清理监听
      const ctx = useSSRContext()!
      ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
    } else {
      return NOOP
    }
  }
  //设置监听的初始值,如果是复合的,那就是[{}],反之就是{}
  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
  // 所谓job就是一次监听执行  
  const job: SchedulerJob = () => {
    if (!effect.active || !effect.dirty) {
      return
    }
    
    if (cb) {
      // 普通watch
      const newValue = effect.run() //其实从这里都能看到effect总是一个同步方法
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // 每次执行都先清理之前的副作用,这里表示值已经发生了改变
        if (cleanup) {
          cleanup()
        }
        // 执行一次回调
        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
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
              ? []
              : oldValue,
          onCleanup,
        ])
        oldValue = newValue
      }
    } else {
      // 这里是watchEffect
      effect.run()
    }
  }

  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb

  let scheduler: EffectScheduler
  if (flush === 'sync') {
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    // post指mounted之后,所以需要延迟一下执行
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    job.pre = true
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job) //二分查找去重,然后插入一个 内部维护的队列
  }
  // 这里正是watch核心代码,也是本文的主要内容
  const effect = new ReactiveEffect(getter, NOOP, scheduler)

  const scope = getCurrentScope()
  // 取消监听的方法
  const unwatch = () => {
    effect.stop()
    if (scope) {
      remove(scope.effects, effect)
    }
  }
  //dev环境下暴露两个勾子,方便调试
  if (__DEV__) {
    effect.onTrack = onTrack
    effect.onTrigger = onTrigger
  }

  // initial run
  // 监听的过程
  if (cb) {
  //如果是watch
    if (immediate) {
      job()
    } else {
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense,
    )
  } else {
  //如果是watchEffect
    effect.run()
  }

  if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
  return unwatch
}

ReactiveEffect

从上面的代码,我们可以很明显的看出,watchEffect的关键就是ReactiveEffect这个类

// 注意这个变量
export let activeEffect: ReactiveEffect | undefined

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = [] // 依赖的

  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean // 是否允许自己触发自己
  // 新暴露的几个勾子,主要是在debugger时候用
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  /**
   * @internal
   */
  _dirtyLevel = DirtyLevels.Dirty
  /**
   * @internal
   */
  _trackId = 0
  /**
   * @internal
   */
  _runnings = 0
  /**
   * @internal
   */
  _shouldSchedule = false
  /**
   * @internal
   */
  _depsLength = 0

  constructor(
    public fn: () => T,
    public trigger: () => void,
    public scheduler?: EffectScheduler,
    scope?: EffectScope,
  ) {
    // 收集当前这个函数至当前scope依赖
    recordEffectScope(this, scope)
  }
  // 这个dirty其实是vue内部的函数,盲猜是更新时根据这个等级去合并更新
  public get dirty() {
    if (this._dirtyLevel === DirtyLevels.MaybeDirty) {
      pauseTracking()
      for (let i = 0; i < this._depsLength; i++) {
        const dep = this.deps[i]
        if (dep.computed) {
          triggerComputed(dep.computed)
          if (this._dirtyLevel >= DirtyLevels.Dirty) {
            break
          }
        }
      }
      if (this._dirtyLevel < DirtyLevels.Dirty) {
        this._dirtyLevel = DirtyLevels.NotDirty
      }
      resetTracking()
    }
    return this._dirtyLevel >= DirtyLevels.Dirty
  }

  public set dirty(v) {
    this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
  }
  // 主要函数,即执行这个依赖的effect函数
  run() {
    this._dirtyLevel = DirtyLevels.NotDirty
    // 如果当前函数不执行,则先执行,这里是节流
    if (!this.active) {
      return this.fn()
    }
    // 储存旧的函数
    let lastShouldTrack = shouldTrack
    // 这里非常重要,把上一次执行的函数保存,然后会修改activeEffect = this
    let lastEffect = activeEffect 
    try {
      shouldTrack = true
      activeEffect = this
      //执行次数+1
      this._runnings++
      // 清理之前的副作用
      preCleanupEffect(this)
      return this.fn()
    } finally {
    //如果报错了
      postCleanupEffect(this)
      this._runnings--
      activeEffect = lastEffect
      shouldTrack = lastShouldTrack
    }
  }

  stop() {
    // 停止监听,这很好理解
    if (this.active) {
      preCleanupEffect(this)
      postCleanupEffect(this)
      this.onStop?.()
      this.active = false
    }
  }
}

activeEffect

从上面的代码阅读结束之后,我认为问题就是出现在activeEffect这个变量,当我们使用async/await时,其实js会把这个函数放到微任务队列里,如果此时有其他的同步effect,可能就会覆盖你的async/await。

因为activeEffect其实是个内部模块导出的全局变量,它可能会被其他的函数修改,也可能会被其他的ReactiveEffect所修改,这是无法追踪的,因此我们应当避免在watchEffect为什么不应该传入异步函数。

结语

解决方案1

在没研究源码之前,我会尽量不使用watchEffect来避免这个bug。大部分时间,url其实只需要获取一次的

解决方案2:改为watch

其实归根结底,这个问题就是你的依赖跟丢了,本来应该数据变化就执行函数,但是数据与执行函数的对应关系丢了。这时候我们就想到了,watch,watch其实是一种强绑定的依赖的,是你手动维护的依赖关系,无论有没有async/await,它都不会丢失。