
文章目录 9
题目
搜索框每次输入都会发送请求:
let data: unknown[] = [];
let loading = false;
let error: string | null = null;
async function search(keyword: string) {
loading = true;
error = null;
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(keyword)}`
);
data = await response.json();
} catch {
error = "搜索失败";
} finally {
loading = false;
}
}用户快速输入 "vue",随后改成 "react"。react 请求先返回,vue 请求后返回,最终页面却展示了 vue 的结果。
请回答:
这段代码存在哪些竞态问题?
如何保证只有最新请求可以修改
data、error和loading?请使用 TypeScript、 和必要的状态保护机制重写它。
为什么只调用
abort()仍不一定足以保证状态一致?应如何区分用户主动取消产生的异常与真正的网络错误?
我的回答
存在搜索竞态问题,短时间内发起的多次请求,无法保证内容呈现最新发起请求的内容
外层赋予一个变量request_id,每次请求赋值其一个唯一的key,保证最新的request_id才执行内容写入;增加AbortController,当发起新的请求,取消旧的请求
let data: unknown[] = [];
let loading = false;
let error: string | null = null;
let abort:AbortController | null = null;
async function search(keyword: string) {
loading = true;
error = null;
if(abort){
abort.abort();
}
const signal = new AbortController();
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(keyword)}`,
{signal}
);
data = await response.json();
} catch {
error = "搜索失败";
} finally {
loading = false;
}
}这里的状态一致不太明白
reject会返回一个一个名为 AbortError 的 DOMException,通过catch中捕获来识别是否未主动取消
题解
整体思路正确,尤其是“请求版本号 + AbortController”的组合。不过代码里有两个关键错误,而且目前只考虑了 data,还没有保证 loading 和 error 的一致性。
评分:6.5 / 10。
正确点
正确识别了“后发请求可能先返回”的竞态问题。
知道可以使用唯一
requestId判断响应是否过期。知道新请求开始时应取消旧请求。
知道取消请求通常会抛出
name === "AbortError"的DOMException。
代码中的问题
首先,创建的 controller 没有保存到外层变量:
const signal = new AbortController();这里变量实际是 AbortController,并不是 AbortSignal。而且没有执行:
abort = new AbortController();其次,fetch 需要接收 controller.signal:
fetch(url, {
signal: abort.signal
});你描述了 request_id,但代码中尚未实现,因此过期响应仍可能更新页面。
“状态一致性”是什么意思?
不仅 data 必须属于最新请求,loading 和 error 也必须属于最新请求。
例如:
请求 A 开始:loading = true
请求 B 开始:取消请求 A,loading = true
请求 A 进入 catch:error = "搜索失败"
请求 A 进入 finally:loading = false
请求 B 仍在等待响应此时就产生了错误状态:
请求 B 明明还在进行,页面却显示
loading = false请求 A 是主动取消的,页面却显示“搜索失败”
因此,请求 A 的 catch 和 finally 也不能随意更新状态。不能只保护下面这一句:
data = result;还需要保护:
error = ...
loading = false为什么只有 abort() 还不够?
abort() 是资源优化和尽早终止机制,不应被当作唯一的状态正确性保障。
可能出现:
旧请求已经完成网络阶段,正在执行
response.json()旧请求已经拿到结果,正在执行后续异步逻辑
旧请求被取消后,它的
catch和finally仍然会执行某些封装库或异步操作并不真正支持取消
因此通常使用两层保障:
AbortController:尽量停止旧请求,节省资源
requestId:禁止过期任务修改最新状态AbortError 判断
你的方向正确,可以更严谨地写成:
if (err instanceof DOMException && err.name === "AbortError") {
// 主动取消,不展示为请求失败
}另外,fetch 遇到 HTTP 404、500 默认不会进入 catch,还需要检查:
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}完整的代码片段
let data: unknown[] = [];
let loading = false;
let error: string | null = null;
let currentRequestId = 0;
let activeController: AbortController | null = null;
async function search(keyword: string): Promise<void> {
// 先让旧请求失效,再取消它。
// 即使取消后旧请求进入 catch/finally,也无法修改最新状态。
const requestId = ++currentRequestId;
activeController?.abort();
const controller = new AbortController();
activeController = controller;
loading = true;
error = null;
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(keyword)}`,
{
signal: controller.signal,
}
);
// fetch 遇到 4xx、5xx 默认不会抛出异常
if (!response.ok) {
throw new Error(`请求失败:HTTP ${response.status}`);
}
const result: unknown = await response.json();
if (!Array.isArray(result)) {
throw new Error("服务端返回的数据格式不正确");
}
// 只有最新请求才能写入数据
if (requestId !== currentRequestId) {
return;
}
data = result;
} catch (cause: unknown) {
// 过期请求不能修改最新请求的 error
if (requestId !== currentRequestId) {
return;
}
// 主动取消不属于业务错误
if (
cause instanceof DOMException &&
cause.name === "AbortError"
) {
return;
}
error = cause instanceof Error
? cause.message
: "搜索失败";
} finally {
// 过期请求不能结束最新请求的 loading
if (requestId === currentRequestId) {
loading = false;
if (activeController === controller) {
activeController = null;
}
}
}
}相关文章
- 面试题-基础
Vue 3 响应式解构:为什么替换 user 后出现“李四 / 王五”
通过一道 Vue 3 代码题,区分 reactive 对象解构、嵌套属性修改与对象替换的行为,并用 toRef 保持源属性关联。文章保留首次作答及追问补答,说明从误判到纠正的过程。
- 面试题-基础
Vue 3 编辑弹窗:草稿状态、取消重开与父子数据契约
以一个始终挂载的姓名编辑弹窗为场景,给出覆盖数据归属、保存取消、重开初始化和外部更新冲突的完整 Vue 3 解法。结合首次回答与递进补答,说明本地草稿、脏标记和版本校验各自解决的问题。
- 面试题-基础
JavaScript 浅拷贝、深拷贝与 structuredClone:前端状态更新的正确方式
通过一道对象复制代码题,梳理 JavaScript 浅拷贝、JSON 深拷贝、structuredClone 和 transferable object 的边界,并解释 React 结构共享与 Vue 3 响应式更新的差异。
评论
0 条评论会在审核后展示,邮箱不会公开。