前言
Vue3版本中关于定义watch相关的有4处,具体有watch、watchEffect、$watch、watch选项。实际上$watch和watch选项这两种方式与Vue2中并没有什么不同,实际上也是为了兼容Vue2.x版本的处理。本文主要关注于watch和watchEffect这两个函数式API,旨在了解相关处理逻辑以及加深对Vue3中副作用effect的细节理解(Vue 3.1.1版本)。
watch
watch API本身是一个函数,实现对指定数据源的侦听,其源码如下:
function watch(source, cb, options) {
if (!isFunction(cb)) { warn(...); }
return doWatch(source, cb, options);
}
其背后是调用doWatch函数来实现相关逻辑,下面会逐步分析doWatch函数的主要逻辑:
处理不同类型source
watch API支持不同方式的源定义,这也是doWatch函数中的核心逻辑之一。相关逻辑如下:
// ref对象
if (isRef(source)) {
getter = () => source.value;
forceTrigger = !!source._shallow;
} else if (isReactive(isReactive)) {
getter = () => source;
deep = true;
} else if (isArray(source)) {
isMultiSource = true;
forceTrigger = source.some(isReactive);
getter = () => source.map(s => { ... });
} else if (isFunction(source)) {
getter = () => { ... };
} else {
getter = NOOP;
warnInvalidSource(source);
}
监听源支持下面4种类型:
- ref对象
- 是否是被reactive处理过的对象
- 数组:支持多个源
- 函数
实际上就是不同类型定义相关的getter函数,其中对于监听多个source其逻辑处理如下:
getter = () => source.map(s => {
if (isRef(s)) {
return s.value;
} else if (isReactive(s)) {
return traverse(s);
} else if (isFunction(s)) {
// 执行函数s
return callWithErrorHandling(s, instance, 2 /* WATCH_GETTER */);
} else {
warnInvalidSource(s);
}
});
其中对于监听源是被reactive处理的对象,需要调用traverse函数来处理,而traverse函数内部的处理逻辑如下:
function traverse(value, seen = new Set()) {
if (!isObject(value) ||
seen.has(value) ||
value["__v_skip" /* SKIP */]) {
return value;
}
seen.add(value);
if (isRef(value)) {
traverse(value.value, seen);
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen);
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v) => {
traverse(v, seen);
});
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], seen);
}
}
return value;
}
看似很复杂实际上就是处理对象中每一个需要存在响应式的属性,为什么需要对reactive处理过的对象这么做呢?
实际上是为了将当前watch对应的effect与深度代理对象的每一个属性建立关联,从而实现当对内部属性进行赋值操作就会触发watch的副作用effect的执行
通过对属性获取就会执行其定义的get操作的函数,其中会触发track函数完成上面的目的。对于复杂结构的完全代理对象,这是一个需要关注的性能点。
deep选项的getter覆盖
这是一个额外的逻辑处理,对于回调函数存在其监听源是被reactive处理的对象,相关getter会被重写,以完成副作用effect的关联:
if (cb && deep) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
创建对应副作用effect
副作用本身是函数式编程中的概念,而在Vue3中effect既是一个概念也是一个函数。所谓的概念是指Vue3中的响应系统中对于赋值后带来的变更处理逻辑,就称之为副作用effect。具体到副作用逻辑的实例则是以函数为载体,即通过调用effect函数来创建一个执行相关副作用的函数。
在watch API中核心的逻辑就是创建副作用effect来实现监听源数据,保证在源数据更新后执行相关回调。具体逻辑如下:
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
});
这里的getter就是执行数据变更后的回调函数,其中非常重要的选项是调度器scheduler。调度器scheduler本身是一个函数,实际上在之前响应系统的文章中就对其有相关说明,在trigger中对应条件下就会effect的调度器就会执行,非常重要逻辑。
实际上doWatch创建effect大部分的逻辑就是根据不同的条件定义其调度器scheduler的逻辑。
支持相关选项
watch API支持的选项有:
- immediate:马上执行
- deep:深度监听
- flush:该选项控制副作用的处理时机,flush存在三个值:sync(同步的)、pre(组件更新前,默认值)、post(组件更新后)
- onTrack 和 onTrigger:开发环境下用于调试监听器行为的相关入口
onTrack和onTrigger函数是属于effect.options,相关的处理是在track和trigger之后。deep选项的支持是针对于被reactive函数处理的对象,immediate和flush的支持逻辑如下:
// 逻辑点1
if (flush === 'sync') {
scheduler = job;
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
} else {
// default: 'pre'
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job);
} else {
job();
}
};
}
// 逻辑点2
if (cb) {
if (immediate) {
job();
} else {
oldValue = runner();
}
}
通过上面的逻辑实际上可知:不同的flush值会设置不同的调度器逻辑。对于flush为post值,实际上调度器执行的逻辑是调用queuePostRenderEffect,而该函数的逻辑如下:
function queueEffectWithSuspense(fn, suspense) {
if (suspense && suspense.pendingBranch) {
if (isArray(fn)) {
suspense.effects.push(...fn);
} else {
suspense.effects.push(fn);
}
} else {
queuePostFlushCb(fn);
}
}
这里不考虑suspense这种特殊的组件处理,普通组件实际上就是调用queuePostFlushCb,而该函数就是将fn推入到pendingPostFlushCbs数组中,而该数组会在queueJob对应位置做处理。这部分的逻辑如下:
function queueJob() {
...
flushPreFlushCbs(seen);
queue.sort((a, b) => getId(a) - getId(b));
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex];
if (job && job.active !== false) {
callWithErrorHandling(job, null, 14 /* SCHEDULER */);
}
}
} finally {
...
flushPostFlushCbs(seen);
...
}
}
flushPostFlushCbs函数就是处理pendingPostFlushCbs数组,而flushPreFlushCbs函数就是处理pendingPreFlushCbs数组。而queue数组存在视图渲染的effect,所以根据flushPreFlushCbs、queue、flushPostFlushCbs这样的处理顺序就保证了组件更新前和组件更新后的处理时机的保证。
返回停止watch的函数
doWatch函数会返回一个用于停止watch的函数,具体逻辑如下:
function stop(effect) {
if (effect.active) {
cleanup(effect);
if (effect.options.onStop) {
effect.options.onStop();
}
effect.active = false;
}
}
return () => {
stop(runner);
if (instance) {
remove(instance.effects, runner);
}
};
而effect onStop的定义实际上只在onInvalidate函数,该函数就是可以实现停止监听后执行传入的回调函数。
watchEffect
watchEffect API支持自定义副作用effect,其相关的处理逻辑如下:
function watchEffect(effect, options) {
return doWatch(effect, null, options);
}
其背后是调用doWatch函数,需要注意的是回调函数为null。在前面的watch API中实际上就会doWatch函数的逻辑有了较为详细的说明。这里梳理下跟watchEffect处理相关的:
function doWatch(source, cb) {
let onInvalidate = (fn) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */);
};
};
if (isFunction(source)) {
getter = () => {
if (instance && instance.isUnmounted) {
return;
}
if (cleanup) { cleanup(); }
return callWithAsyncErrorHandling(source, instance, 3, [onInvalidate]);
};
}
if (cb) {
...
} else if (flush === 'post') {
queuePostRenderEffect(runner, instance && instance.suspense);
} else {
runner();
}
}
watchEffect API的形参source必然是函数,那么watchEffect是如何收集依赖呢?实际上就是runner执行的逻辑,runner实际上就是通过effect函数创建的一个函数即activeEffect,该函数中会真正执行所谓的副作用逻辑。当执行runner函数时,就会执行watchEffect的source函数,这个过程中所有响应属性就会与当前activeEffect(即这里的runner)建立关联,这样就完成了所谓的依赖收集。当收集的属性被重新赋值,依据建立的联系就会触发副作用effect被执行,这就是watchEffect的功能了。
总结
watchEffect API可以自动实现对相关数据源的监听,而watch API实现对数据源的监听需要指定,这两个API都是惰性执行副作用。这两个API背后都是调用内部函数doWatch函数来实现的,相关选项的支持程度也是不同的:
- watchEffect API:支持flush、onTrack、onTrigger,配置immediate、deep没有意义
- watch API:支持所有选项