[Vue] 竞态问题
竞态问题通常在多线程编程中被提及。
多线程中的竞态问题(Race Condition)是指多个线程或进程在并发执行时,由于对共享资源(如变量、内存、文件等)的访问和修改顺序不确定,导致程序行为不可预测的问题。
竞态问题的关键点在于:
- 共享资源:多个线程同时访问并修改同一个资源,如全局变量或内存位置。
- 执行顺序不确定:由于线程调度的不确定性,不同线程执行的顺序可能会不同,导致修改共享资源时产生意外结果。
前端工程师可能很少讨论它,但在日常工作中你可能早就遇到过与竞态问题相似的场景,比如下面的例子:
这里我们又一个简单的共享资源finalData
,并且有两次对于响应式数据的操作,由于我们设置了监听,所以,每次响应式数据的改变,watch
都会运行,那么也就都会触发耗时操作complexOption
。这里我故意设置了第二次请求B的相应速度比第一次请求A快。这会导致请求B先于请求A返回。最终导致finalData
中存储的会是第一次请求A的结果
<div id="layer1"></div>
<div id="layer2"></div>
<div id="layer3"></div>
<button id="btn1">修改1</button>
<button id="btn2">修改2</button>
<script type="module" src="index.js"></script>
const layer1 = document.querySelector("#layer1");
const layer2 = document.querySelector("#layer2");
const layer3 = document.querySelector("#layer3");
const btn1 = document.querySelector("#btn1");
const btn2 = document.querySelector("#btn2");
let count = 0;
// 耗时操作
async function complexOption(n) {
return new Promise((resolve, reject) => {
setTimeout(
() => {
resolve(n === 1 ? 100 : 200);
},
n === 1 ? 2000 : 500
);
});
}
let finalData = ref(0);
watch(proxy, async (newValue, oldValue) => {
const res = await complexOption(count);
finalData.value = res;
});
effect(() => {
layer1.innerHTML = proxy.value.name;
layer3.innerHTML = finalData.value;
});
btn1.addEventListener("click", () => {
count = 1
proxy.value.name = "王五";
});
btn2.addEventListener("click", () => {
count = 2
proxy.value.name = "赵六";
});
但由于请求 B 是后发送的,因此我们认为请求 B 返回的数据才是最新的,而请求 A 则应该被视为过期的,所以我们希望变量 finalData 存储的值应该是由请求 B 返回的结果,而非请求 A 返回 的结果
实际上,请求 A 是副作用函 数第一次执行所产生的副作用,请求 B 是副作用函数第二次执行所产 生的副作用。由于请求 B 后发生,所以请求 B 的结果应该被视为最新的,而请求 A 已经过期了,其产生的结果应被视为无效。通过这种方式,就可以避免竞态问题导致的错误结果。
归根结底,我们需要的是一个让副作用过期的手段。简单来说,如果出现了多次请求,那么在下一次请求之前,我们需要先清除之前的请求。当然,要清除,首先就是要注册,不然你在哪里去找到清除?
因此,我们用cleanup
来存储用户注册的过期的回调函数,但其实我们处理的可以更加简单,让用户在调用watch的时候进行注册onInvalidate
函数,这个函数中的回调函数需要赋值给cleanup
,其实这个回调函数可以写的很简单,就是设置一个标识expired
,当这个标识为true
,也就是只要cleanup
有值,并且执行了,就标识我们需要忽略上一次的结果。直接保留最后一次执行的结果即可。
function watch(source, cb, options = {}) {
let getter;
if (isFunction(source)) {
getter = source;
} else {
getter = () => traverse(source);
}
// 定义旧值与新值
let oldValue, newValue;
// cleanup用来存储用户注册的过期回调
let cleanup;
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn;
}
// 将调度函数scheduler内执行的代码单独封装为job函数
const job = () => {
newValue = effectFn();
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 将onInvalidate作为回调函数的第三个参数,供用户使用
cb(newValue, oldValue, onInvalidate);
oldValue = newValue;
};
// 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到
// effectFn 中以便后续手动调用
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: job,
});
if (options.immediate) {
job();
} else {
oldValue = effectFn();
}
}
const proxy = ref(obj);
let count = 0;
// 耗时操作
async function complexOption(n) {
return new Promise((resolve, _reject) => {
setTimeout(
() => {
resolve(n === 1 ? 100 : 200);
},
n === 1 ? 2000 : 500
);
});
}
let finalData = ref(0);
watch(proxy, async (newValue, oldValue, onInvalidate) => {
let expired = false;
onInValidate(() => {
expired = true;
})
const res = await complexOption(count);
if (!expired) {
finalData.value = res;
}
});
effect(() => {
layer1.innerHTML = proxy.value.name;
layer3.innerHTML = finalData.value;
});
btn1.addEventListener("click", () => {
count = 1
proxy.value.name = "王五";
});
btn2.addEventListener("click", () => {
count = 2
proxy.value.name = "赵六";
});