先记住这个答案
创建一个 AbortController,将 signal 传入 fetch。当获得 response.body 的 reader 后,监听 signal 的 abort 事件:调用 reader.cancel() 使读取循环退出,并捕获 AbortError 进行资源清理。在 async 读取函数中,abort 会导致 reader.read() 立即 reject,因此需在 catch 中判断错误类型,区分主动取消与网络错误。
- abort 后
reader.read()会 rejectAbortError - 主动调用
reader.cancel()可释放锁与连接 - 捕获
AbortError并做差异处理
abort 信号如何中断正在进行的读取
当调用 controller.abort() 时,fetch 返回的 Promise 会以 AbortError 拒绝。若此时已通过 response.body.getReader() 获得 reader,那么正在等待的 reader.read() 也会被拒绝,同样抛出 AbortError。这是因为 abort 会向底层网络层发出取消指令,使流进入错误状态。因此,代码内需要在 catch 中区分此错误,避免将其视为普通网络故障。
但仅依赖 read() 的异常是不够的,规范并没有要求浏览器在 abort 时自动调用 reader.cancel()。为使流对象上的锁被立即释放,应在 signal 的 abort 事件回调中显式调用 reader.cancel()。cancel() 返回一个 Promise,可忽略结果,但它会触发底层关闭流程。如果未调用 cancel(),reader 可能保持锁定,导致后续无法重新绑定同一响应数据流,并延长连接释放时间。
带停止按钮的流式聊天补全
假设聊天页面有一个“停止生成”按钮。用户点击发送后,前端创建 AbortController,将 signal 传给 fetch 并发起流式请求。读取循环把每个 value 解码成文本并追加到消息框。按钮点击事件调用 controller.abort(),同时更新界面状态为“已停止”。
为确保循环及时退出,需在 abort 事件里调用 reader.cancel()。虽然 abort 会使挂起的 read() 被拒绝,但显式调用 cancel() 可确保读锁被立即释放,减少实现差异带来的资源滞留。在 catch 中判断 error.name === 'AbortError' 后,执行清理如隐藏加载指示、释放引用。注意若用户快速连点,应禁用按钮或每次重新生成新的 AbortController,避免取消掉后续请求。
abort 的有效条件与常见失败原因
如果响应已经读取完毕(read() 返回 {done:true}),再调用 abort() 不会引发任何事件,也不会影响已收到的内容。如果服务端已经发送了完整响应但客户端尚未读完,abort 会中断连接,但可能已产生计费,这是取消的固有代价。abort 无法撤销已暴露给用户的文本。
多请求并发时,每个请求必须持有独立的 AbortController。若多个请求复用同一个 signal,取消一个会误杀全部。另外,abort() 只能触发一次,多次调用无效。如果 fetch 因网络错误而失败,abort 不会重复触发,此时应单独处理。对于已取消的请求,不能再复用该 response 对象,需要重新发起请求。
容易答错的地方
- 只调用 abort 不处理 reader
- 很多实现以为
abort()会自动关闭流,但实际只让read()抛出AbortError。若不调用reader.cancel(),底层套接字可能不会立即关闭,且 reader 锁未释放。应在 abort 监听器中主动 cancel 并捕获异常。 - 把 AbortError 当普通错误处理
- 有的代码 catch 里将所有错误都按网络问题弹窗提示,导致用户点击停止后出现错误提示。应通过
error.name === 'AbortError'区分主动取消,静默处理并进入停止状态。否则会造成困扰且无法正确恢复。
面试官还会怎么问?
取消后能否继续从断点续传?
不能。fetch 的流式响应不支持续传,信号中断后连接已关闭。若需恢复,应保存已显示的文本,用新请求模拟继续,或使用 SDK 内置的恢复模式。
如果 abort 发生在 read() 返回之后但未处理前,会怎样?
若 abort 在两次 read 之间,下一次 read() 会立刻 reject AbortError;若 abort 在 read 成功返回后,Promise 已解析,abort 不会影响该次结果。
AbortController 可以被多次 abort 吗?
不能。abort() 方法只触发一次事件,后续调用会被忽略。若需再次取消同一请求,需创建新的 AbortController。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。