前言
大家好,最近有朋友发出疑问 说在vue
的响应式API
文档中,watch
监听函数里面执行异步方法时,为什么需要手动调用watch
回调函数返回的第三个参数onCleanup
方法,它的原理又是什么?这其实就是为了解决竟态问题 。下面我将先说明什么是竟态问题,以及出现问题的场景,并给出解决办法。对竟态问题详细了解后再手写一个简单的watch
监听函数,探索onCleanup
的实现原理,帮助你更好的了解vue
在封装watch
方法时是怎么解决竟态问题的。
竟态问题定义
竞态问题,也叫竞态条件(Race Condition
),是指一个系统或进程的输出依赖于不受控制的事件出现顺序或出现时机。
换一种角度来理解就是存在多个相同请求时,无法保证先请求的先返回就导致了返回结果错乱问题。
竟态问题场景
举个例子------输入框搜索
当我们每次输入每一个字母时,都会发送一个请求获取当前关键词的相关结果,但如果这时由于网络原因,前面的请求还没完成,继续输入关键词发送请求,则会存在多个并发请求,这时如果后面发送的请求比前面的请求先完成了,就会导致返回结果顺序错乱,拿不到预期的结果,也就是所谓的竞态问题。
解决方案
这里不讨论在请求中间层做处理的解决方案,因为这不是本文的重点,下面解决方案将结合vue
中的watch
监听方法进行讲解。
1.中断请求:在下一个请求开始前取消上一个请求;
2.忽略请求结果:在下一个请求开始前,标记忽略上一个请求的返回结果;
在前面例子中,也许大家会想到利用防抖,在指定时间内如果连续触发请求,只执行一次。但这并不能解决根本问题。因为你无法预知网络延迟时间,超出设定的防抖时间范围再次触发时,如果前面的请求还没有拿到返回结果还是会出现问题。
watch执行异步方法如何解决竞态问题
请求中断
下图例子来自vue
官方文档,其实就是在用到了中断请求的方法。
如果你使用的是最新版的axios
,以下代码可以模拟以上例子的doAsyncWork
方法
js
export const doAsyncWork = (newId) => {
// 创建中断请求控制器
const controller = new AbortController();
const response = axios.get('/user', {
params: {
id: newId
},
signal: controller.signal
})
return {
// 返回中断请求方法
cancel: () => controller.abort(),
response
}
}
这个例子其实就是每次id
改变时,发送请求前先中断前一个请求。这时可能会有同学问,你明明传入onCleanup
方法中的cancel
方法不是本次请求的吗?为什么会取消上一次的请求?了解watch
方法实现原理的同学应该都知道怎么一回事,不了解也没关系,下面我们简单手撸一个watch
方法说明其中的原理。
watch
监听方法中的参数onCleanup方法解析
js
// 手写一个简单的watch监听方法
function watch(source, cb, options = {}) {
// ...省略前面watch实现细节代码
// 定义新值和旧值
let newValue, oldValue
// 定义保存执行onCleanup时传入的回调方法,在本例中就是保存取消请求方法cancel
let cleanup
// 清除副作用方法
function onCleanup(fn) {
cleanup = fn
}
// 假设watch监听值改变时会触发这个方法
const job = () => {
newValue = getNewValue()
/**
* 执行watch的回调方法前先看看有没有需要执行的副作用方法(在这为中断请求方法)
* 第一次触发watch监听回调时cleanup为空,所以不会中断请求
* cb回调执行后,onCleanup(cancel)调用会保存本次中断请求方法到cleanup变量中
* 当第二监听回调触发前,执行的cleanup保存的为上一次的请求中断方法
* 执行后会重新赋值为本次的请求中断方法
*/
if(cleanup) {
cleanup()
}
// 执行传入的回调方法,cb方法为传入watch的第二个参数
cb(newValue, oldValue, onCleanup)
oldValue = newValue
}
// 获取新值
function getNewValue() {
// ...省略实现细节
}
// ...省略后面watch实现细节代码
}
重点: 其中主要是理解onCleanup
方法在watch
方法中只是担任保存用户传入的中断请求方法的角色。
忽略请求
忽略请求主要是利用闭包 的原理,在每次监听回调执行时都会在该方法中创建一个expired
变量标记当前请求是否过期,然后把过期方法传入onCleanup
中,待下次再次触发回调时,修改上一次回调闭包中的expired
变量为true
js
watch(keyword, (async (newValue, oldValue, onCleanup) => {
// 标记上一个请求是否过期
let expired = false
onCleanup(() => {
expired = true
})
const res = await getSeachResult(newValue)
// 赋值时判断请求是否过期,如果过期则忽略赋值
if(!expired) {
result.value = res
}
}), {
immediate: true
})
总结
以上我们通过vue
的watch API
引入了什么是竞态问题 ,并给出了竞态问题的定义,接着例举了一个可能会出现该问题的场景,并说明可用于解决该问题的两个方案,最后回到本文主题,说明在watch
监听回调方法中如何解决该问题,以及手写一个简单的watch
方法说明在解决该问题上的实现原理,最后列举了在使用watch
时,如何使用提到的两种方案解决该问题的例子。