Axios 取消请求的底层原理

493 阅读4分钟

Axios 的请求取消机制本质上是基于浏览器提供的 AbortController API(早期版本使用CancelToken ,但现已逐渐被 AbortController 取代)。它的核心原理是通过 中断网络请求 来实现取消,但需要明确以下几点关键细节:

Axios 取消请求的原理

1. 浏览器层的信号传递

  • AbortController 本质是一个「信号发射器」: 创建 const controller = new AbortController() 时,浏览器会生成一个带有signal属性的对象。 signal是一个AbortSignal对象,它具备监听「终止事件」的能力。
  • 将signal绑定到请求
axios.get('/api', { signal: controller.signal })

此时 Axios 会将这个signal传递给浏览器底层的fetch API 或 XMLHttpRequest(根据适配器类型)。

CancelToken 的兼容方案(旧版):

  • 通过 CancelToken.source() 生成一个 source 对象。

  • 将 source.token 传递给请求配置。

  • 调用 source.cancel() 时,Axios 会抛出 Cancel 错误并中断请求。

2. 中断请求的触发

  • 调用controller.abort()
    • 触发signal的abort事件,并标记 signal.aborted 为 true。
    • 浏览器底层(如 fetch)会立即 终止与该信号关联的网络活动
  • 具体中断行为
    • 如果请求尚未完成(例如处于 pending 状态),浏览器会强制关闭 TCP 连接。
    • 如果响应正在接收中,浏览器会停止读取响应流。

3. 错误反馈机制

  • 请求被取消时,Axios 会抛出一个 CanceledError(旧版为 Cancel 错误)。
  • 通过 axios.isCancel(error) 可判断是否为取消触发的错误:
.catch(error => {
  if (axios.isCancel(error)) {
    console.log('请求被主动取消');
  }
});

二、为什么“已发出的请求无法取消”?

网络层的不可逆性

    • 一旦 HTTP 请求的请求体数据已经开始传输,浏览器无法从客户端“撤回”已发送的字节。
    • 取消操作只能终止尚未发送的数据放弃接收响应,但服务器可能已经处理了部分数据。

2. 底层网络行为的限制

  • TCP 连接的中断
    • abort() 本质是 强制关闭 TCP 连接,但无法保证服务器已经接收的数据量。
    • 如果数据仍在传输中,浏览器会触发 ECONNABORTED 错误,但服务器可能已接收部分内容。
  • UDP 无连接特性(不适用于 HTTP):
    • HTTP 基于 TCP,而 TCP 是面向连接的可靠协议,中断后无法恢复。

3. 大文件上传的特殊性

  • 如果使用单次请求上传整个大文件,请求一旦发出,客户端只能断开连接,但服务器可能已经接收了部分内容。
  • 对于分块上传(chunked upload),可以取消尚未发送的分块,但已成功上传的分块无法撤回。

三、如何正确实现「可取消的大文件上传」

方案一:分块上传(Chunked Upload)

  • 实现步骤

  • 优点
    • 已上传的分块可保留在服务器(结合后端支持断点续传)。
    • 未上传的分块可立即终止。

方案二:后端事务性处理

  • 流程设计
    1. 前端开始上传时,先请求后端生成一个「临时上传会话 ID」。
    2. 上传过程中,携带该 ID 标识当前上传任务。
    3. 取消时,调用后端 API 通知删除与该 ID 关联的临时数据。

  • 优点
    • 彻底清理服务器上的残留数据。
    • 避免存储无效的上传内容。

代码示例:分块上传 + 动态取消

// 分块上传管理器
class ChunkedUploader {
  constructor(file, chunkSize = 1024 * 1024) {
    this.file = file;
    this.chunkSize = chunkSize;
    this.chunks = [];
    this.currentChunk = 0;
    this.abortControllers = [];
    this.uploadSessionId = null;
  }

  // 初始化分块
  prepareChunks() {
    const totalChunks = Math.ceil(this.file.size / this.chunkSize);
    for (let i = 0; i < totalChunks; i++) {
      const start = i * this.chunkSize;
      const end = Math.min(start + this.chunkSize, this.file.size);
      this.chunks.push(this.file.slice(start, end));
    }
  }

  // 上传单个分块
  async uploadChunk(chunk) {
    const controller = new AbortController();
    this.abortControllers.push(controller);

    await axios.post('/upload-chunk', {
      chunk,
      sessionId: this.uploadSessionId,
      index: this.currentChunk,
    }, {
      signal: controller.signal,
    });

    this.currentChunk++;
  }

  // 开始上传
  async start() {
    const { data } = await axios.post('/create-upload-session');
    this.uploadSessionId = data.sessionId;
    this.prepareChunks();

    for (const chunk of this.chunks) {
      await this.uploadChunk(chunk);
    }

    // 所有分块上传完成后通知后端合并
    await axios.post('/complete-upload', { sessionId: this.uploadSessionId });
  }

  // 取消上传
  cancel() {
    this.abortControllers.forEach(controller => controller.abort());
    axios.post('/cancel-upload', { sessionId: this.uploadSessionId });
  }
}

// 使用示例
const uploader = new ChunkedUploader(file);
uploader.start();

// 点击取消按钮
document.getElementById('cancel').addEventListener('click', () => {
  uploader.cancel();
});

四、注意事项

  1. 取消的局限性
    • 只能取消尚未完成的请求,无法回滚已到达服务器的数据。
    • 如果请求已处于pending 状态但未开始传输数据(如 DNS 解析阶段),可成功取消。
  2. 错误处理
    • 使用 axios.isCancel(error) 判断错误是否由取消触发。
  3. 性能优化
    • 对于大文件上传,分块+并行上传+进度监控是最佳实践。

关键总结

  1. Axios 取消的本质:通过浏览器 API 强制中断网络连接,但对已传输的数据无控制权。
  2. 大文件上传的解决方向
    • 分块上传:细粒度控制每个分块的取消。
    • 后端协作:清理临时数据,支持断点续传。
  3. 实际开发建议
    • 优先使用 AbortController(现代浏览器支持)。
    • 对旧版浏览器,回退到 CancelToken(需注意 Axios 版本兼容性)。