Skip to content

请求取消与过期结果 ​

搜索框会产生一串异步请求。取消旧请求可以节省工作,但不能单独保证旧结果不会发布:请求可能已经完成,或者运行环境根本不支持取消。因此要同时使用取消信号和请求序号。

最小实验:控制请求完成顺序 ​

下面的 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(),否则请求完成后仍可能尝试写入已不存在的视图。

练习 ​

  1. 给 fakeSearch 增加失败路径,并让网络错误进入可见错误状态、AbortError 保持安静。
  2. 把输入改成连续三个查询,记录每个请求的开始、取消和发布状态。
  3. 说明为什么仅禁用搜索按钮不能替代底层序号守卫。

查看本节答案

用心记录每一步成长