watch中的onCleanup:
无论是watch还是watchEffect,可以发现在函数中都可以接受一个onCleanup,watch的cleanup是回调的第三个参数,watchEffect()是回调的第一个参数,看看文档对cleanUp这个函数的描述:
watch:
watchEffect:
文档的描述是注册清理副作用函数的回调函数,那么为什么要注册清理副作用函数的回调函数呢?为什么副作用函数要被清理呢?
副作用函数过期问题:
用watch看一个例子:
watch(source, async (old, new, OnCleanup) => {
// 是否过期是标志
let expired = false
// 注册过期回调
OnCleanup(()=> {
expired = true
})
const res = await fetch('something')
// 如果未过期,那么可以取res为finalData
if (!expired) {
finalData = res
}
})
从上面代码可以看到,source改变后会执行回调,但是回调中有异步任务。假设这一次fetch的返回值为A,在异步任务完成之前很有可能source又一次改变了,这时又会触发callback重新执行,又会发送一次fetch请求,假设这次fetch的返回值为B,那么A和B哪个会先返回呢,答案是不确定的,很有可能B比A先返回也有可能A比B先返回。但是无论是谁先返回结果,都会造成最终finalData的混乱,我们并不知道finalData的值是哪次请求得到的。
但是毕竟B是比A更新的请求,应该保留的结果是B。所以理想情况下是,无论执行多少次callback,都要取最新的那一次请求。
所以这就是通过Onleanup注册的清理回调的作用,每次source改变之后,在运行callback之前都会执行清理回调。运行清理回调之后,通过闭包保存的expired会被设置为true,所以这一次的请求结果就不会被赋给finalData。只有没过期时才会采用请求结果。
那么Vue是如何实现这一点呢?
注册清理回调:
基本的原理就和之前说过的那样,需要在每次响应式数据变更之后,callback执行之前,调用注册过的清理回调,清理上一次过期的副作用函数,比如上面例子中的等待中的异步请求。把上篇文章的watch拿过来改造一下:
function watch(source, callback) {
let getter
if (typeof source === 'function') {
// 如果source是getter
if (callback) {
getter = source
} else {
// source为getter还可能是watchEffect(无callback)
// 将cleanup和source封装为getter
getter = () => {
if (cleanup) {
cleanup()
}
source()
}
}
} else if (Array.isArray(source)) {
// 如果是数组
getter = () =>
source.map((val) => {
// 我只处理了数组中只有普通对象和getter
if (typeof source === 'function') {
return val()
} else {
traverse(val)
}
})
} else {
getter = () => {
traverse(source)
}
}
let value
let prevValue
let cleanup
// 清理回调注册函数
function Oncleanup(fn) {
cleanup = fn
}
// 前面讲到过,如果lazy为true,
// 就返回副作用函数的包装函数,这个包装函数返回副作用函数的值
const effectFn = effect(
() => {
// 执行封装好的getter,读取数据
getter()
},
{
lazy: true,
scheduler() {
// 说明是watch
if (callback) {
// 调度函数内执行callback
value = effectFn()
// watch为懒执行,只有在第一次数据变化触发callback时才会注册cleanup
// callback执行之前清理回调,清理回调只会在第一次callback执行之后注册
if (cleanup) {
cleanup()
}
callback(value, prevValue, Oncleanup)
prevValue = value
} else {
// watch为了兼容watchEffect,在调度函数内嵌套执行effectFn()
// cleanup与source封装在一起
effectFn(Oncleanup)
}
}
}
)
if (callback) {
// 因为懒执行,所以手动调用进行读取操作建立响应联系
// 数据变更调用scheduler的时候,会重新拿到新值
// 这里不会注册cleanup,因为watch的effectFn只是它的getter
prevValue = effectFn()
} else {
// watchEffect首次运行并收集依赖
// wachEffect执行的effectFn本身就是他自己的callback,所以会立刻注册回调
effectFn(Oncleanup)
}
}
在上面的代码中我们分别对watch和watchEffect的部分做了改造,让他们在首次执行时能够注册清理回调, 同时在每次响应式依赖改变之后,副作用函数运行之前,触发注册的清理回调。这样用户就有机会对过期的回调“做标记”,实现清理过期副作用的目的。