Vue3 源码阅读(7)- watch
watch
第三个参数可以传入 options
对象,对象中 flush
属性可以控制回调的更新时机,分别是组件更新前、组件更新后 和 同步触发
从这里得知该 api 与组件渲染有关联,所以其实现不在 @vue/reactivity
里,而是在 @vue/runtime-core
中
具体路径为 packages/runtime-core/src/apiWatch.ts
watch
我们跳转到 watch
实现,可以发现其具体实现是 doWatch
方法
1 | export function watch<T = any, Immediate extends Readonly<boolean> = false>( |
一如既往的在生产环境下有控制台提示
watchEffect
直接就是返回一个 doWatch
的执行结果
1 | export function watchEffect( |
doWatch
该方法是 watch
,watchEffect
的核心实现
总共一百多行,涉及到了挺多东西
接收三个参数:
source
: 观察的源数据,可以是一个方法、单个对象、对象数组cb
:watch
的监听回调options
: 各种配置
1 | function doWatch( |
某些配置需要回调
1 | if (__DEV__ && !cb) { |
生产环境下,如果没有回调,但是填入了 immediate
或 deep
选项,会警告用户
初始化变量
源数据错误的提示方法
1
const warnInvalidSource = (s: unknown) => { ... };
当前的组件实例
1
const instance = currentInstance;
getter
和一些 Flag1
2
3let getter: () => any;
let forceTrigger = false;
let isMultiSource = false;getter
函数通过我们传入的source
源数据来实现,用于构建副作用forceTrigger
标记是否需要强制触发其副作用isMultiSource
标记是否为多个源数据(数组)
构建 getter
如果
source
为ref
1
2
3
4if (isRef(source)) {
getter = () => source.value;
forceTrigger = isShallow(source);
}getter
直接返回ref.value
,也就是监听value
的变化如果为
shallowRef
,为了兼顾triggerRef
接口,需要强制触发其副作用(非常迷幻的特性)如果是响应式的对象
1
2
3
4else if (isReactive(source)) {
getter = () => source;
deep = true;
}getter
直接返回其本身,并且deep
强制为true
如果是数组(多个源数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16else if (isArray(source)) {
isMultiSource = true;
forceTrigger = source.some((s) => isReactive(s) || isShallow(s));
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 {
__DEV__ && warnInvalidSource(s);
}
});
}isMultiSource
设为true
并且只要里面有任意
reactive
,shallow
的项,则需要强制触发副作用getter
:如果是
ref
,返回ref.value
,跟上面单个数据源的处理一致如果是响应式的对象即
reactive
处理过的,则getter
会手动遍历其所有属性,遍历是为了track
每一个属性,起到深度监听的效果如果
deep
为true
(上面那种情况),后续也会这样处理如果是函数,则直接调用它
其它情况都是不合法的数据源,直接报错
如果是函数(
getter
函数)分为有回调
cb
,和无回调的情况1
2
3
4else if (isFunction(source)) {
if (cb) { ... }
else { ... }
}有回调
对应的是调用
watch
的情况1
2getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER);getter
直接返回source
执行的结果没有回调
对应调用
watchEffect
的情况1
2
3
4
5
6
7
8
9
10
11
12
13
14getter = () => {
if (instance && instance.isUnmounted) {
return;
}
if (cleanup) {
cleanup();
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
);
};直接就是一个普通的副作用实现
cleanup
是用户传入的一个钩子函数这里可能会觉得奇怪,为什么
watch
不用执行cleanup
,其实后面的实现中会补上
啥也不是
那就啥也不是,直接设置
getter
为一个空函数1
getter = NOOP;
NOOP
的实现 :() => {}
后面会出现处理 vue2 兼容的处理和 SSR 的处理,我们直接无视
deep 实现
很简单,暴力遍历一遍所有属性来 track
其变化
1 | if (cb && deep) { |
cleanup
该钩子会在清理副作用时调用
1 | let cleanup: () => void; |
用户通过暴露出去(后续会暴露)的 onCleanup
方法,传一个函数,该函数会被赋值到 cleanup
和副作用的 onStop
钩子上
初始化 oldValue
1 | let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE; |
如果为多数据源,则为一个数组,否则为一个全局固定的对象
调度任务
1 | const job: SchedulerJob = () => { ... } |
这玩意主要实现 flush
的功能,即调用时机,会涉及到 vue3 渲染时的三个队列
如果当前副作用已经被停止,不执行
1
2
3if (!effect.active) {
return;
}有回调
1
if (cb) { ... }
即
watch
的情况如果有变化、
deep
或forceTrigger
为true
1
2
3
4
5
6
7
8
9
10
11
12if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) { ... }先调用
cleanup
钩子1
2
3if (cleanup) {
cleanup();
}接着调用回调,回调第三个参数暴露了
onCleanup
方法1
2
3
4
5
6callWithAsyncErrorHandling(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
,供下次使用1
oldValue = newValue;
否则啥也不干
如果没有回调
就是
watchEffect
的情况,直接调用副作用即可1
2
3
4else {
// watchEffect
effect.run();
}
最后如果有回调,则该任务是允许递归调用的(回调中触发自身 trigger
)
1 | job.allowRecurse = !!cb; |
调度任务分配队列
1 | let scheduler: EffectScheduler; |
如果为
sync
同步调用直接使用
job
本身即可post
将该任务推送到组件渲染后执行的队列中延迟执行
pre
将该任务推送到组件渲染前执行的队列中延迟执行
这里面的实现我抽取一段来说:
vue3 渲染时有三个队列,除了刚刚
pre
和post
之外,还有个中间队列preQueue
,queue
,postQueue
组件渲染都是在
queue
中进行的为了实现
watch
,watchEffect
合并多个更新的特性使用了事件循环中微任务的特性,我们如果深入的去看队列的实现会发现 :
1
2
3
4
5
6function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}这里
resolvedPromise
就是Promise.resolve()
,利用这里执行flushJobs
函数,将所有任务推入到微任务队列中我们代码中连续的
xxxx.a = 1
之类的修改操作(即宏任务)执行完后,会一次性执行所有微任务(这里是执行flushJobs
函数,里面会按照三个队列的顺序依次执行) :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function flushJobs(seen?: CountMap) {
...
// 执行 pre 队列中的任务
flushPreFlushCbs(seen);
...
try {
// 执行中间队列里的任务
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex];
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue;
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER);
}
}
} finally {
...
// 执行 post 队列中的任务
flushPostFlushCbs(seen);
...
}
}最后我们点进
flushPreFlushCbs
方法看下实现你会惊奇的发现非常的朴实无华:
1
2
3
4
5
6
7
8
9
10
11
12...
// 利用 Set 去重
activePreFlushCbs = [...new Set(pendingPreFlushCbs)];
for (
preFlushIndex = 0;
preFlushIndex < activePreFlushCbs.length;
preFlushIndex++
) {
...
activePreFlushCbs[preFlushIndex]();
}
...简简单单的利用
Set
去重,结合上面的微任务队列,达到了合并操作的目的
构建副作用
1 | const effect = new ReactiveEffect(getter, scheduler); |
初始化副作用依赖
1 | // initial run |
前面解读 effect
的实现时我们发现,必须调用 effect.run
触发 track
行为,后续才会响应变化
这里不过是针对不同情况用不同的方法去调用
如果有回调且需要立刻执行,就直接执行任务
job
(里面有调用effect.run
)有回调但是不用立刻执行,则只追踪变化顺带计算
oldValue
初始值没有回调且时机为
post
,则将副作用推入queuePost
队列,触发更新时,组件渲染结束后才延迟调用副作用没有回调且时机为
pre
,sync
,直接调用副作用追踪变化
停止监听方法
最后会返回一个停止监听的方法
1 | return () => { |
停止副作用
如果当前组件实例有
scope
作用域,把当前副作用从作用域中移除
调用流程
比如下面代码,我们从 watch
开始走一遍流程
1 | const target = ref(1); |
传入
ref
,所以watch
执行后会立刻调用一次effect.run
,追踪target.value
的变化执行
add
方法,target.value
值变动,调用了trigger
,该trigger
会调用watch
内部的effect.scheduler
调度器该调度器将任务(上面提到的
调度任务
)推入preQueue
,并设置一个微任务,该微任务会按顺序执行所有队列中的任务到此宏任务执行完了,开始执行微任务,微任务会先取
preQueue
队列中的任务执行,取到了我们的job
在
job
中通过effect.run
计算出新值,并调用watch
传入的回调