请求取消与过期结果
搜索框会产生一串异步请求。取消旧请求可以节省工作,但不能单独保证旧结果不会发布:请求可能已经完成,或者运行环境根本不支持取消。因此要同时使用取消信号和请求序号。
最小实验:控制请求完成顺序
下面的 fakeSearch 用定时器模拟服务器,完整页面不需要后端。把代码保存为 race.html 后直接打开:
html
<input id="query" placeholder="输入 tea 或 coffee">
<pre id="log"></pre>
<script>
const input = document.querySelector('#query')
const log = document.querySelector('#log')
let sequence = 0
let controller
function fakeSearch(query, { signal }) {
const delay = query === 'tea' ? 500 : 100
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve({ query, items: [`${query}-1`] }), delay)
signal.addEventListener('abort', () => {
clearTimeout(timer)
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'))
}, { once: true })
})
}
async function search(query) {
controller?.abort()
controller = new AbortController()
const current = ++sequence
log.textContent += `start ${current}: ${query}\n`
try {
const result = await fakeSearch(query, { signal: controller.signal })
if (current !== sequence) return
log.textContent += `publish ${current}: ${result.items.join(',')}\n`
} catch (error) {
if (error.name !== 'AbortError') log.textContent += `error ${current}: ${error.name}\n`
}
}
input.addEventListener('input', event => search(event.target.value))
</script>先输入 tea,100 毫秒内再输入 coffee。日志应只发布 coffee-1;tea 要么被取消,要么因序号过期而不能写入。删除 controller?.abort(),再运行一次,观察序号守卫仍能阻止旧结果。
机制与边界
取消是资源控制,序号是发布控制。服务器已经收到的写请求不会因为客户端 abort 自动回滚,所以写操作仍需要幂等键或服务端去重。组件卸载时还要调用 controller.abort(),否则请求完成后仍可能尝试写入已不存在的视图。
练习
- 给
fakeSearch增加失败路径,并让网络错误进入可见错误状态、AbortError 保持安静。 - 把输入改成连续三个查询,记录每个请求的开始、取消和发布状态。
- 说明为什么仅禁用搜索按钮不能替代底层序号守卫。