Vue3源码学习——侦听器watchEffect
上一节,我们介绍了watch函数的逻辑,watch函数接收三个参数,分别是:
- 监听对象
- 回调函数
- 配置项
当监听对象发生改变的时候,会触发回调函数的执行。
这一节,我们来看一下侦听器章节的另一个API——watchEffect。
watchEffect
先看一下watchEffect的用法:
定义: 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
watchEffect可以接收两个参数:
- 第一个参数就是要运行的副作用函数。
- 第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。(等同于watch的配置项参数)
例如:
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 输出 0
count.value++
// -> 输出 1
在上面例子中,watchEffect传入一个打印count.value的函数
,那么Vue3就会自动监听count.value
,当count.value
发生改变时,就会在控制台打印count.value
的值。
接下来,我们正式进入源码阶段:
首先还是老样子,我们找到最直接的——watchEffect这个函数:
function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
怎么样,是不是似曾相识呢?跟我们上一节讲到的watch简直一模一样,都是直接返回doWatch函数。只不过这次向doWatch函数中的传递的第二个参数cb
传为了null
。
我们接着看doWatch函数逻辑:
getter
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
const instance = currentInstance
let getter;
let forceTrigger = false;
let isMultiSource = false;
// 这里会根据传入的source类型,为getter赋不同的值
if (isRef(source)) {
...
} else if (isReactive(source)) {
...
} else if (isArray(source)) {
...
} else if (isFunction(source)) {
// watchEffect传入的source是function类型,同时cb为null,所以我们只看这里的逻辑即可
if (cb) {
...
} else {
getter = () => {
// 组件是否被卸载
if (instance && instance.isUnmounted) {
return;
}
// 是否存在cleanup函数(通常用于清除上一轮没有执行完的watchEffect)
if (cleanup) {
cleanup();
}
// 返回执行结果,并将onCleanup作为参数传递给source
return callWithAsyncErrorHandling(
source,
instance,
3,
[onCleanup]
);
};
}
} else {
...
}
}
我们在watch函数章节有提到,doWatch函数首先做的就是将不同类型的监听对象source处理成一个通用的getter,从而能够在后续阶段将getter包装为一个副作用函数effect,并执行,完成依赖收集,就不再重复这一块内容,有兴趣可以再回顾一下 侦听器watch 这一章节。
而在watchEffect函数中,因为传入doWatch函数的source
是function类型,cb
为null,所以执行的getter赋值逻辑如上。
onCleanup
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
// getter赋值
...
if (cb && deep) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
let cleanup;
let onCleanup = (fn) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */);
};
};
...
}
getter函数确定之后,接下来定义了onCleanup函数。这里onCleanup函数会接收一个函数
作为参数,将该函数的执行控制赋值给cleanup
。而我们在上面getter的逻辑中可以看到,在执行source
之前,都会判断一次是否存在cleanup
,如果存在则优先执行cleanup
。
这里有点绕,我们举个例子:
const state = ref(1)
function increment() {
state.value++
}
watchEffect((onCleanup) => {
let x = state.value
let timer = setTimeout(() => {
console.log(state.value)
}, 2000)
onCleanup(() => {
clearTimeout(timer)
})
})
先说一下这个例子的执行情况:
- 当我们低频率执行
increment
时(执行间隔大于2秒):控制台每次会延迟2秒打印出增加后的state.value值 - 当我们高频率执行
increment
时(执行间隔小于2秒):控制台只会在最后一次执行完成后延迟2秒打印出state.value的值。
我们来分析一下这个例子:
- 首先,向watchEffect函数中,传入了一个函数参数fn,函数fn的内容是延迟2秒在控制台打印出
state.value
的值。 - 这个函数fn可以接收一个onCleanup函数作为参数,这一点我们在源码中对应的是
callWithAsyncErrorHandling( source, instance, 3, [onCleanup] );
- 我们往onCleanup函数中,传入了清空定时器的操作,这样也就是说,当执行完fn之后,会为我们开启一个定时器,同时doWatch函数源码中的
cleanup
会赋值为清空当前的定时器的函数。 - 当我们快速执行increment的时候,首先会执行cleanup函数,也就是会清空掉我们上一个watchEffect开启的定时器。所以,最后控制台只会打印出我们最后一次执行完increment之后
state.value
的值。 - 这里的逻辑,可以理解为就是用watchEffect实现的防抖。
onCleanup函数的作用不止于此,也可以用于取消一些无效请求。
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel) data.value = await response
})
job
关于onCleanup函数我们就说到这里,继续往下看,我们这一节只关注watchEffect中关于创建job这一块的逻辑:
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
// getter赋值
...
// onCleanup
...
// watchEffect中关于创建job不再需要维护新老值,直接执行副作用函数effect就完了
const job = () => {
if (!effect.active) {
return;
}
effect.run();
};
// watchEffect不允许递归调用
job.allowRecurse = false;
// 确定调度
let scheduler;
if (flush === "sync") {
scheduler = job;
} else if (flush === "post") {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
} else {
job.pre = true;
if (instance)
job.id = instance.uid;
scheduler = () => queueJob(job);
}
// 将我们上面包装好的getter处理为副作用函数effect
const effect = new ReactiveEffect(getter, scheduler);
// 调试侦听器时可用
if (true) {
effect.onTrack = onTrack;
effect.onTrigger = onTrigger;
}
if (flush === "post") {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
);
} else {
effect.run();
}
...
}
通过隐藏无关逻辑,我们可以发现,doWatch函数在cb
为null时的逻辑还是比较清晰的:
- 将
getter
处理为副作用函数effect,同时根据调度程序scheduler确定job
在任务队列中的执行时机。 - 创建执行任务
job
。这里job的逻辑就非常简单,直接执行副作用函数effect。 - 当传入的配置项中
flush
不为post时,执行副作用函数effect,执行过程中会涉及到对属性的访问,完成对函数中涉及到的响应式属性的依赖收集。 - 后续,当watchEffect函数中收集过的属性发生改变时,即会触发副作用函数的执行,完成监听。
unwatch
最后同样也是取消监听的函数:
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
...
const unwatch = () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
return unwatch
}
定义一个取消监听的函数,通过执行effect.stop方法移除收集到的对应的副作用函数,并将unwatch方法暴露出去,这样我们就可以通过在外界调用从而取消监听了。
转载自:https://juejin.cn/post/7246241550338310199