竟态问题的解决方案(vue3中onInvalidate的实现思路)

问题描述

比如现在有一个需求是,页面上有一个数据列表,每当点击列表中的一项时,就会发起一个请求 A 来更新一个数据 res,再点击第二次,发起一个新的请求 B。如果请求 A 由于某些原因,延迟比较大,而 B 很快就返回来了,这就会导致 A 会在 B 之后完成并更新数据,此时 res 中的数据就是上一次点击的结果而并非正确的数据。这就是竟态问题了。

vue3给出的解决方法

首先来看 vue3 中是怎么解决这个问题的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
watch(
() => obj.type,
(newVal, oldVal, onInvalidate) => {
let isExpired = false // 是否过期
onInvalidate(() => { isExpired = true })

// 这里 res 拿到的就是 simulationRequest 中生成的 random 随机数
const res = await simulationRequest() // 模拟一个网络请求,下面的例子中会有

// 如果 isExpired 过期了就不再更新数据了
if (!isExpired) {
// data 总能保证是拿到的是最近的操作获得的数据,而不是最久延迟获得的数据
data = res
console.log(data);
}
}
)

watch 的第二个参数是一个回调函数 callback,而这个 callback 的第三个参数就是一个钩子函数 onInvalidate,这个钩子函数的作用就是,在下一次更新数据 obj.type 之前执行 onInvalidate 中的参数方法。因此如果出现文章开头问题描述中的问题,那么后一次的请求 B 执行之前就会调用这个钩子函数,使得 A 请求过期,这样就能保证拿到的结果是正确的。

原生js中如何实现

那么如果在 js 中应该怎么解决这个问题呢,实际上上面的思路已经有了,就是通过一个变量来控制本次请求的结果是否过期来实现的,问题就是原生 js 中没有 onInvalidate 这个钩子函数。

因此我们的思路应该是,onInvalidate 是如何实现的。在 vue3 中,watch 是通过 effect 以及 scheduler 调度函数来实现的,不过这里我们不考虑如何实现一个 watch,只考虑 onInvalidate 的实现,用它来解决我们的竟态问题。

思路重点如下:

  • onInvalidate 是用来更新过期函数(让上一次请求的结果过期)的
  • 过期函数一定要在数据更新之前执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Document</title>
</head>

<body>
<button id="btn">模拟发送网络请求</button>
<script>
let data = null // 存储最终需要的数据

let clearup; // 保存过期函数

/**
* 钩子函数,用于更新过期函数
*/
function onInvalidate(fn) {
clearup = fn
}

// 添加点击
const btn = document.getElementById('btn')
btn.onclick = () => {
// 如果存在过期函数,则执行一次过期函数,这样就能让上一次异步数据过期
if (clearup) {
clearup()
}
// 过期函数一定要在下一次操作开始之前执行
handleClickLiItem(onInvalidate)
}

/**
* 点击事件的回调
* @onInvalidate: function 是一个钩子函数,用于更新过期函数,使得每次执行点击事件都能使上一次的数据过期
*/
async function handleClickLiItem(onInvalidate) {
let isExpired = false // 是否过期
onInvalidate(() => {
isExpired = true
})

// 这里 res 拿到的就是 simulationRequest 中生成的 random 随机数
const res = await simulationRequest()

// 如果 isExpired 过期了就不再更新数据了
if (!isExpired) {
// data 总能保证是拿到的是最近的操作获得的数据,而不是最久延迟获得的数据
data = res
console.log(data);
}
}

/**
* 模拟延迟场景
*/
function simulationRequest() {
let random = Math.floor(Math.random() * 10) + 1
let result;
console.log('本次延迟' + random + '秒');

let p = new Promise((resolve) => {
setTimeout(() => {
result = random
resolve(result)
}, random * 1000)
})

return p
}
</script>
</body>

</html>

在上面的例子中:

  • 过期函数对应的就是 clearup
  • 数据更新则对应的是 handleClickLiItem 这个方法

可以明显的看到,过期函数是在‘数据更新’发生之前执行的。

当第一次点击按钮的时候,会先判断是否有国企函数,因为第一次进来的时候还并没有给 cleanup 赋值,自然就不会有也不会执行过期函数,然后执行了按钮的点击事件对应的回调方法 handleClickLiItem,这个方法会携带一个钩子函数 onInvalidate,在调用 onInvalidate 的时候就会更新过期函数 clearup,这个过期函数的作用就是将请求 A 的过期状态变量 isExpired 设置为过期状态 true ,之后会发送请求 A,这里我们假设 A 延迟了 10 秒。之后,在这 10 秒结束之前,我们再次点击按钮,此时因为 cleanup 已经有对应的内容了(设置 A 为过期状态),因此就会执行这个过期回调,使得 A 过期。然后再执行按钮的点击事件对应的回调方法,发送请求 B,这样就能保证即使 BA 之前返回来也没有关系。


竟态问题的解决方案(vue3中onInvalidate的实现思路)
https://silengzi.github.io/cube-fluid-blod/2022/07/26/7124561155468558367/
作者
silengzi
发布于
2022年7月26日
许可协议