Vue Reactive源码🗣watch & watchEffect

889 阅读4分钟

watchEffect

作用: 用来追踪响应式依赖,并在追踪的时候自动触发一次,后序检测到响应式依赖的话,会再次更新,注意所有的里面的响应式数据(ref\reactive) 都会自动被加入依赖中

用法: 传入一个副作用方法,并会自动追踪里面的响应式依赖,另外还可以传入一个可选的options,用于控制副作用触发的时机:pre|post|sync,分别是组件渲染前后和同步,默认是pre

watch

作用: 用来检测指定data源的变化;

用法: 用于检测的对象可以是一个ref,也可以是一个getter方法,也可以是一个数组,然后传入一个回调,参数可以获取新旧值,另外可以配置options来设置{immediate,deep}

源码时间 ⏰

因为两者共用发放,所以一起看,

// watchEffect
export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}

// watch  利用函数重载来实现类型优化
// 我们可以清楚的看到watch的三种传参方式: 
// ref,
// 一个reactive对象(如果想监听某个属性通过getter函数),
// 监听多个data的数组方式

export function watch<
  T extends Readonly<Array<WatchSource<unknown> | object>>,
  Immediate extends Readonly<boolean> = false
>(
  sources: T,
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

// overload #2: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
  source: WatchSource<T>,
  cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

// overload #3: watching reactive object w/ cb
export function watch<
  T extends object,
  Immediate extends Readonly<boolean> = false
>(
  source: T,
  cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  if (__DEV__ && !isFunction(cb)) {
    warn(
      `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
        `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
        `supports \`watch(source, cb, options?) signature.`
    )
  }
  return doWatch(source as any, cb, options)
}

可以看到watchEffectwatch事实上都是返回了doWatch这个方法,下面就带着问题一起探索一下这个方法

watchEffect 的实现

  • watchEffect如何自动收集依赖的?

首先看一下我们给doWatch传入的参数:

doWatch(effect, null, options)

然后我们大概看一下doWatch的格式

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle {// ... }

可以看到总共接受4个参数,一个被检测源,一个回调函数,一个配置项,一个实例项且默认当前实例 返回值是WatchStopHandle类型,其实就是一个没有返回值的函数,即:

type WatchStopHandle = ()=>void

然后我们就对号入座,首先我们给出一个例子

const count = ref(0)
watchEffect(()=>console.log(count.value))

所以对应依次是:

  • source : ()=>console.log(count.value)
  • cb: null
  • options: null
  • instance default

然后进入方法内部去查看,首先是一个条件判断

 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.`
      )
    }
  }

这里我们就清楚了cb是用来区别是watch还是watchEffect,如果没有传入回调函数,那就是watchEffect,并且同时我们也不能指定immediate,deep这两个配置项,所以正好对应了官网文档说的:

watch 与 watchEffect 在手动停止,副作用无效 (将 onInvalidate 作为第三个参数传递给回调),flush timing 和 debugging 有共享行为。即(flush, onTrack, onTrigger)

然后会有进一步的判断:主要是区别source的类型,是ref怎么处理,是reactive对象怎么处理,是数组怎么处理,我们目前只关注watchEffect,所以只看传入函数这一项,结合注释看一下

  let getter: () => any

else if (isFunction(source)) {
    if (cb) {     //如果有回调函数 那就是watch了
    // watch ....
    } else {
      // no cb -> simple effect
      getter = () => {  // 这里就是watchEffect的判断
        if (instance && instance.isUnmounted) { 
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  }

首先我们查看当前实例有没有挂在到dom上,cleanup是清除副作用的,通过接受onInvalidate参数来传入的回调函数,这主要用来 在执行异步请求这种异步任务时,在执行完毕以前依赖的状态发生变化时处理,主要在re-run的时候或者组件卸载的时候执行

然后我们看一下是如何收集依赖的:

watch

首先看一下watch可以传入的参数:

 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.`
    )