竞态问题
约 701 字大约 2 分钟
2025-11-04
问题
什么是竞态问题?
如何解决竞态问题?
竞态问题 是指多个异步操作的执行顺序不确定,导致最终结果依赖于它们完成的相对时间,从而产生非预期行为。
常见场景
- 快速输入内容,后发起的请求先返回,覆盖了先发起但后返回的正确结果。
- 多次点击按钮触发多个相同请求。
示例
HTML
<input autocomplete="off" type="text" id="query" placeholder="输入搜索词..." />
<div id="result"></div>javascript
// 模拟不确定请求:延迟 0.5 ~ 2 秒
function mockRequest(query) {
const delay = 500 + Math.random() * 1500;
return new Promise((resolve) => {
setTimeout(() => {
resolve(`显示结果: "${query}" (实际耗时 ${delay.toFixed(0)}ms)`);
}, delay);
});
}
const input = document.getElementById("query");
const resultEl = document.getElementById("result");
input.addEventListener("input", async (e) => {
const q = e.target.value.trim();
if (!q) {
resultEl.textContent = "";
return;
}
resultEl.textContent = `请求已发出: "${q}"...`;
// 没有取消机制:每个输入都发请求,且都会更新 UI
const result = await mockRequest(q);
resultEl.textContent = result; // 可能被“旧但晚到”的请求覆盖
});css
/* css 代码 */在上面的例子当中,我们使用 mockRequest 模拟一个耗时的异步请求,请求时间 0.5 秒到 2 秒之间,每输入一个搜索词,会触发一个请求。
可以发现,当用户输入多个搜索词时,多个请求会并发发出,实际结果会显示耗时最短的。
解决方案
在 JS 当中,可以使用 AbortController 来取消过期的请求。
HTML
<input autocomplete="off" type="text" id="query" placeholder="输入搜索词..." />
<div id="result"></div>javascript
// 模拟不确定请求:延迟 0.5 ~ 2 秒
function mockRequest(query, signal) {
const delay = 500 + Math.random() * 1500;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (signal?.aborted) {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
} else {
resolve(`结果: "${query}" (耗时 ${delay.toFixed(0)}ms)`);
}
}, delay);
});
}
const input = document.getElementById("query");
const resultEl = document.getElementById("result");
let abortController = null;
input.addEventListener("input", async (e) => {
const q = e.target.value.trim();
resultEl.textContent = "加载中...";
// 取消上一个请求
if (abortController) abortController.abort();
abortController = new AbortController();
try {
const res = await mockRequest(q, abortController.signal);
resultEl.textContent = res;
} catch (err) {
if (err.name !== "AbortError") {
resultEl.textContent = "请求出错";
console.error(err);
}
// 若是 AbortError,说明是过期请求,静默丢弃
}
});css
/* css 代码 */示例中,我们使用了 AbortController 来取消过期的请求。请求发起之前,先取消上一次的请求,然后创建一个新的 AbortController,并绑定到当前请求。这样,当用户输入新的搜索词时,上一个请求将被取消,新的请求将开始执行,最终显示结果将总是最后一次请求结果。
在 Vue3 中,使用 watch 或者 watchEffect 来触发请求也需要注意竞态问题,可以使用 onCleanup() 忽略过时请求。在 React 中,可以在 Effect 中返回一个清理函数,来处理竞态问题。