4.11 过期的副作用
异步回调存在竞态问题,用
onInvalidate注册「过期回调」,让被淘汰的那次副作用结果不再落地。
一、竞态问题
典型场景是两个异步请求的返回顺序被打乱:
- 第一次修改
obj的字段 → 回调执行 → 发出请求 A; - A 还没返回,字段又被修改 → 回调再次执行 → 发出请求 B;
- 若 B 先于 A 返回,最终
finalData里存的却是 A 的结果 —— 数据被「过期」的副作用覆盖了。
二、onInvalidate:注册过期回调
watch(obj, async (newValue, oldValue, onInvalidate) => {
// 标志:当前副作用是否已过期
let expired = false
// 注册过期回调
onInvalidate(() => {
expired = true // 过期时置为 true
})
// 发送网络请求(模拟)
const res = await new Promise(resolve => {
console.log('发送请求')
setTimeout(() => resolve(obj.foo), Math.random() * 1000)
})
// 只有没有过期,才使用请求结果
if (!expired) {
console.log('请求结果:', res)
}
})
obj.foo++ // 第一次修改
setTimeout(() => obj.foo++, 200) // 200ms 后再修改
三、实现原理
思路:每次检测到变更、在副作用函数重新执行之前,先执行上一次通过 onInvalidate 注册的过期回调。
// cleanup 用来存储用户注册的过期回调
let cleanup
// 用户注册过期回调(即把回调存起来)
function onInvalidate(fn) {
cleanup = fn
}
const job = () => {
newValue = effectFn()
// 调用回调 cb 之前,先执行过期回调
if (cleanup) {
cleanup()
}
// onInvalidate 作为第三个参数传给用户
cb(newValue, oldValue, onInvalidate)
oldValue = newValue
}
四步拆解:
| 步骤 | 做法 |
|---|---|
| 1 | 声明 cleanup 变量,存放用户注册的过期回调 |
| 2 | onInvalidate(fn) 只是把 fn 赋值给 cleanup |
| 3 | job 中在调用 cb 之前,若 cleanup 存在就先执行它 |
| 4 | 把 onInvalidate 作为 cb 的第三个参数传出去供用户使用 |
时序:第二次变更触发 job → 先执行 cleanup() → 第一次执行时闭包里的 expired 变成 true → 请求 A 返回时结果被丢弃。
四、易错点
cleanup()必须在cb()之前调用;否则「上一次」的过期回调会晚于本次回调执行,失去意义。expired通过闭包保存在每次回调执行的独立作用域中,因此只影响那一次副作用,不会误伤新的一次。
相关
- 4.10 立即执行的 watch 与回调执行时机:
immediate与flush