一、watch 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数

第一个参数可以是不同形式的 “数据源”:

它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

 

第二个参数是在发生变化时要调用的回调函数:

这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数

 

第三个可选的参数是一个对象,支持以下这些选项:

immediate:是否立即执行回调
deep:是否深度监听
flush:调度的时机
onTrack / onTrigger:调试侦听器的依赖
once:回调函数只会运行一次

 

  • 基本使用
1const x = ref(0)
2const y = ref(0)
3
4// Single ref
5watch(x, (newX) => {
6  console.log(`x is ${newX}`)
7})
8
9// Getter function
10watch(
11  () => x.value + y.value,
12  (sum) => {
13    console.log(`sum of x + y is: ${sum}`)
14  }
15)
16
17// Array of multiple sources
18watch([x, () => y.value], ([newX, newY]) => {
19  console.log(`x is ${newX} and y is ${newY}`)
20})

 

  • 实现原理 (packages/runtime-core/src/apiWatch.ts)

doWatch 是 watch 的核心实现函数,负责处理各种情况:

1// implementation
2export function watch<T = any, Immediate extends Readonly<boolean> = false>(
3  source: T | WatchSource<T>,
4  cb: any,
5  options?: WatchOptions<Immediate>,
6): WatchStopHandle {
7  /// ...
8  return doWatch(source as any, cb, options)
9}

 

Getter 函数的创建:

这里处理不同类型的 source,生成相应的 getter 函数。getter 函数的作用是提取出需要被监听的值

如果 source 是 ref,则 getter 返回 source.value。如果是 reactive 对象,则使用 reactiveGetter 处理。NOOP 是一个空操作函数。等等,以此类推

1function doWatch(
2  source: WatchSource | WatchSource[] | WatchEffect | object,
3  cb: WatchCallback | null,
4  {
5    immediate,
6    deep,
7    flush,
8    once,
9    onTrack,
10    onTrigger,
11  }: WatchOptions = EMPTY_OBJ,
12): WatchStopHandle {
13  /// ...
14
15  let getter: () => any
16  let forceTrigger = false
17  let isMultiSource = false
18
19  if (isRef(source)) {
20    getter = () => source.value
21    forceTrigger = isShallow(source)
22  } else if (isReactive(source)) {
23    getter = () => reactiveGetter(source)
24    forceTrigger = true
25  } else if (isArray(source)) {
26    /// ...
27  } else if (isFunction(source)) {
28    /// ...
29  } else {
30    getter = NOOP
31    __DEV__ && warnInvalidSource(source)
32  }
33
34  /// ...
35}

 

调度和执行:

job 是当数据变化时被调度执行的函数

当数据变化时,effect.run() 会执行 getter,并判断是否需要触发回调函数

如果有回调函数 cb,则在变化时调用它,并传入新的值和旧的值

1function doWatch(
2  source: WatchSource | WatchSource[] | WatchEffect | object,
3  cb: WatchCallback | null,
4  {
5    immediate,
6    deep,
7    flush,
8    once,
9    onTrack,
10    onTrigger,
11  }: WatchOptions = EMPTY_OBJ,
12): WatchStopHandle {
13  /// ...
14
15  const job: SchedulerJob = () => {
16    if (!effect.active || !effect.dirty) {
17      return
18    }
19    if (cb) {
20      // watch(source, cb)
21      const newValue = effect.run()
22      if (
23        deep ||
24        forceTrigger ||
25        (isMultiSource
26          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
27          : hasChanged(newValue, oldValue)) ||
28        (__COMPAT__ &&
29          isArray(newValue) &&
30          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
31      ) {
32        // cleanup before running cb again
33        if (cleanup) {
34          cleanup()
35        }
36        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
37          newValue,
38          // pass undefined as the old value when it's changed for the first time
39          oldValue === INITIAL_WATCHER_VALUE
40            ? undefined
41            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
42              ? []
43              : oldValue,
44          onCleanup,
45        ])
46        oldValue = newValue
47      }
48    } else {
49      // watchEffect
50      effect.run()
51    }
52  }
53
54  /// ...
55}

 

ReactiveEffect 的使用 (通过创建 ReactiveEffect 来监听数据的变化是 Watch 函数实现的原理):

通过 flush 选项决定调度方式

immediate 选项: 如果 immediate 为 true,那么在 watch 初始化时会立即执行 job,这意味着在第一次渲染时就会执行回调

如果 immediate 为 false,则通过 effect.run() 手动执行一次 getter,以捕获初始值

最后,doWatch 函数返回 unwatch 函数,这样调用者可以随时停止监听

1function doWatch(
2  source: WatchSource | WatchSource[] | WatchEffect | object,
3  cb: WatchCallback | null,
4  {
5    immediate,
6    deep,
7    flush,
8    once,
9    onTrack,
10    onTrigger,
11  }: WatchOptions = EMPTY_OBJ,
12): WatchStopHandle {
13  /// ...
14
15  let scheduler: EffectScheduler
16  if (flush === 'sync') {
17    scheduler = job as any // the scheduler function gets called directly
18  } else if (flush === 'post') {
19    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
20  } else {
21    // default: 'pre'
22    job.pre = true
23    if (instance) job.id = instance.uid
24    scheduler = () => queueJob(job)
25  }
26
27  const effect = new ReactiveEffect(getter, NOOP, scheduler)
28
29  const scope = getCurrentScope()
30  const unwatch = () => {
31    effect.stop()
32    if (scope) {
33      remove(scope.effects, effect)
34    }
35  }
36
37  if (__DEV__) {
38    effect.onTrack = onTrack
39    effect.onTrigger = onTrigger
40  }
41
42  // initial run
43  if (cb) {
44    if (immediate) {
45      job()
46    } else {
47      oldValue = effect.run()
48    }
49  } else if (flush === 'post') {
50    queuePostRenderEffect(
51      effect.run.bind(effect),
52      instance && instance.suspense,
53    )
54  } else {
55    effect.run()
56  }
57
58  if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
59  return unwatch
60}

 

二、watchEffect 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行

第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调

第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖

1const count = ref(0)
2
3watchEffect(() => console.log(count.value))
4// 0
5
6count.value++
7// 1

 

  • 实现原理 (packages/runtime-core/src/apiWatch.ts)

可以发现 doWatch 同时是 watch 和 watchEffect 的核心实现函数,它根据不同的参数执行不同的逻辑

effect 参数则是一个副作用函数,cb(回调函数)为 null,所以这是一个纯副作用的监听逻辑

1export type WatchEffect = (onCleanup: OnCleanup) => void
2
3// Simple effect.
4export function watchEffect(
5  effect: WatchEffect,
6  options?: WatchOptionsBase,
7): WatchStopHandle {
8  return doWatch(effect, null, options)
9}

 

doWatch 函数中与 watchEffect 相关的关键部分:

当 watchEffect 被调用时,doWatch 会立即运行 effect.run(),执行副作用函数,并收集所有在执行过程中使用到的响应式依赖

核心原理也是通过 ReactiveEffect 实现的,ReactiveEffect 会在副作用函数执行时自动收集依赖,并在依赖变化时重新执行该函数

1/// ...
2
3const effect = new ReactiveEffect(getter, NOOP, scheduler)
4
5/// ...
6
7// initial run
8if (cb) {
9  if (immediate) {
10    job()
11  } else {
12    oldValue = effect.run()
13  }
14} else if (flush === 'post') {
15  queuePostRenderEffect(
16    effect.run.bind(effect),
17    instance && instance.suspense,
18  )
19} else {
20  effect.run()
21}
22
23/// ...

 

三、watch 与 watchEffect() 相比

watch 和 watchEffect 的使用示例::

1import { ref, watch } from 'vue';
2
3const count = ref(0);
4
5watch(count, (newValue, oldValue) => {
6  console.log(`count changed from ${oldValue} to ${newValue}`);
7});
1import { ref, watchEffect } from 'vue';
2
3const count = ref(0);
4
5watchEffect(() => {
6  console.log(`count is now ${count.value}`);
7});

 

使用 watch 时,你需要明确指定哪些数据需要监听,并且可以通过回调函数获取新旧值来对数据变化进行精确处理

使用 watchEffect 时,Vue 会自动追踪所有在副作用函数中使用到的响应式数据,并在它们发生变化时重新执行整个函数,适合处理简单的自动化副作用

watch 适用于需要精确控制何时以及如何响应数据变化的场景,watchEffect 则更加自动化,所有在副作用函数中使用到的响应式数据都会被自动追踪

 

四、其他

Tips:

(1) 关于 ReactiveEffect,在本文中不会继续深入,所以如果你是初次阅读 Vue 3 源码,请务必先了解 ReactiveEffect,在 Watch 中我们可以发现 ReactiveEffect 是从 @vue/reactivity → './effect' 中导入的,你可以从这个包中找到,最好可以结合 reactive 实现一起阅读

(2) 在阅读的时候,建议了解实现的基本核心逻辑就可以,意思就是先考虑代码执行的正常情况,额外情况先考虑跳过,因为 Vue 源码里面各种条件判断特别多,而这些判断很多时候是为处理各种情况,有时候我们没有考虑到这么多种情况,容易跟不下去

 

学习的记录,仅供参考