Web Abort API - AbortSignal 与 AbortController

AbortSignal 和 AbortController 是 Web 标准中用于取消异步操作的 API。它们提供了一种统一的方式来中止 fetch 请求、事件监听器、以及其他异步操作。

  • AbortController 是一个控制器对象,它允许你在需要时中止一个或多个异步操作。
  • AbortSignal 是一个信号对象,表示某个操作是否应该被中止。它由 AbortController 创建并暴露给异步操作使用。

为什么需要 Abort

在 Abort API 出现之前,取消异步操作是一个困难的问题:

  1. fetch 请求无法取消:一旦发起 fetch 请求,就无法在中途取消,即使用户已经导航到其他页面
  2. 各种异步 API 取消方式不统一:XMLHttpRequest、setTimeout、addEventListener 等都有不同的取消机制
  3. 资源浪费:无法取消的请求会浪费网络带宽和服务器资源
  4. 内存泄漏风险:未清理的异步操作可能导致内存泄漏

Abort API 提供了一个统一、标准的解决方案。

javascript 复制代码
// 创建一个 AbortController 实例
const controller = new AbortController();

// 获取 signal 对象
const signal = controller.signal;

// 使用 signal 发起 fetch 请求
fetch('/api/data', { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求被取消');
    }
  });

// 在某个时刻取消请求
controller.abort();

API 详解

ts 复制代码
interface AbortController {
  readonly signal: AbortSignal; // 返回一个 AbortSignal 对象实例,用于与异步操作通信
  abort(reason?: any): void; //  中止操作,可选地提供一个中止原因
}

interface AbortSignal extends EventTarget {
  readonly aborted: boolean; // 指示关联的异步操作是否已被中止
  onabort: ((this: AbortSignal, ev: Event) => any) | null; // 当关联的 AbortController 调用 abort() 时触发
  readonly reason: any; // 中止的原因,默认为 DOMException(name: 'AbortError')
  throwIfAborted(): void; // 如果信号已中止,则抛出中止原因
}

var AbortSignal: {
  prototype: AbortSignal;
  new(): AbortSignal; // 在浏览器中是受保护的,不能直接调用。会抛出异常 「Illegal constructor」
  abort(reason?: any): AbortSignal; // 返回一个已经中止的 AbortSignal
  any(signals: AbortSignal[]): AbortSignal; // 返回一个新的 AbortSignal,当数组中任何一个 signal 中止时,它也会中止
  timeout(milliseconds: number): AbortSignal; // 返回一个将在指定时间后自动中止的 AbortSignal
};

源码实现

以下是 AbortController 和 AbortSignal 的简化实现,帮助理解其工作原理:

typescript 复制代码
class AbortSignal extends EventTarget {
  private _aborted: boolean = false;
  private _reason: any = undefined;

  get aborted(): boolean {
    return this._aborted;
  }

  get reason(): any {
    return this._reason;
  }

  throwIfAborted(): void {
    if (this._aborted) {
      throw this._reason;
    }
  }

  // 内部方法:触发中止
  _signalAbort(reason: any): void {
    if (this._aborted) return;

    this._aborted = true;
    this._reason = reason ?? new DOMException('signal is aborted', 'AbortError');

    // 触发 abort 事件
    this.dispatchEvent(new Event('abort'));
  }

  // 静态方法:创建已中止的 signal
  static abort(reason?: any): AbortSignal {
    const signal = new AbortSignal();
    signal._signalAbort(reason);
    return signal;
  }

  // 静态方法:创建超时的 signal
  static timeout(milliseconds: number): AbortSignal {
    const signal = new AbortSignal();
    setTimeout(() => {
      signal._signalAbort(new DOMException('signal timed out', 'TimeoutError'));
    }, milliseconds);
    return signal;
  }

  // 静态方法:组合多个 signal
  static any(signals: AbortSignal[]): AbortSignal {
    const resultSignal = new AbortSignal();

    // 检查是否已有 signal 被中止
    for (const signal of signals) {
      if (signal.aborted) {
        resultSignal._signalAbort(signal.reason);
        return resultSignal;
      }
    }

    // 监听所有 signal 的 abort 事件
    const onAbort = function(this: AbortSignal) {
      resultSignal._signalAbort(this.reason);
      // 清理其他监听器
      for (const signal of signals) {
        signal.removeEventListener('abort', onAbort);
      }
    };

    for (const signal of signals) {
      signal.addEventListener('abort', onAbort);
    }

    return resultSignal;
  }
}

class AbortController {
  private _signal: AbortSignal;

  constructor() {
    this._signal = new AbortSignal();
  }

  get signal(): AbortSignal {
    return this._signal;
  }

  abort(reason?: any): void {
    this._signal._signalAbort(reason);
  }
}

关键设计要点

  1. 单向通信:AbortController 通过 signal 单向通知异步操作,异步操作不能修改 signal 状态
  2. 事件驱动:使用 EventTarget 接口,通过事件机制通知监听者
  3. 不可逆性:一旦 signal 被中止,就无法恢复
  4. 原因传递:支持传递中止原因,便于错误处理和调试
  5. 受保护的构造函数:AbortSignal 的构造函数在浏览器中是受保护的,不能直接调用,只能通过特定的工厂方法创建,这确保了 signal 的创建和管理符合规范

实际应用场景

1. 取消 Fetch 请求

最常见的用途是取消网络请求:

javascript 复制代码
const controller = new AbortController();

// 开始请求
fetch('/api/large-data', { signal: controller.signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求被用户取消');
    } else {
      console.error('请求失败:', err);
    }
  });

// 用户点击取消按钮
cancelButton.addEventListener('click', () => {
  controller.abort('用户点击取消按钮');
});

解决的问题

  • 避免用户离开页面后仍在后台下载数据
  • 减少不必要的网络流量
  • 改善用户体验,让用户可以取消长时间运行的请求

2. 请求超时控制

使用 AbortSignal.timeout() 实现请求超时:

javascript 复制代码
// 5秒后自动超时
fetch('/api/data', {
  signal: AbortSignal.timeout(5000)
})
  .then(response => response.json())
  .catch(err => {
    if (err.name === 'TimeoutError') {
      console.log('请求超时');
    }
  });

解决的问题

  • 防止请求无限期挂起
  • 为慢速或无响应的服务器设置时间限制
  • 提高应用响应性

3. 清理事件监听器

addEventListener 支持 signal 参数,当 signal 中止时自动移除监听器:

javascript 复制代码
const controller = new AbortController();

// 监听滚动事件
window.addEventListener('scroll', handleScroll, {
  signal: controller.signal
});

// 组件卸载时清理
onUnmount(() => {
  controller.abort();
});

解决的问题

  • 防止内存泄漏
  • 简化事件监听器的清理逻辑
  • 避免手动调用 removeEventListener

4. 组合多个中止条件

使用 AbortSignal.any() 组合多个中止条件:

javascript 复制代码
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);

// 用户取消或超时,都会中止请求
fetch('/api/data', {
  signal: AbortSignal.any([
    userController.signal,
    timeoutSignal
  ])
}).catch(err => {
  console.log('请求被中止:', err.message);
});

// 用户点击取消
cancelButton.addEventListener('click', () => {
  userController.abort();
});

解决的问题

  • 灵活组合多个中止条件
  • 支持复杂的控制流程
  • 避免创建多个 controller 的复杂逻辑

5. React/Vue 组件中的异步操作

在组件中使用 AbortSignal 防止内存泄漏:

javascript 复制代码
// React 示例
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => setUser(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error('加载用户失败:', err);
        }
      });

    // 清理函数
    return () => controller.abort();
  }, [userId]);

  return <div>{user?.name}</div>;
}

解决的问题

  • 组件卸载后中止正在进行的请求
  • 防止在已卸载的组件上设置状态
  • 避免竞态条件(race condition)

6. 可中止的自定义异步操作

在自定义异步函数中支持 AbortSignal:

javascript 复制代码
async function processLargeFile(file, signal) {
  const chunks = splitFile(file);

  for (const chunk of chunks) {
    // 检查是否已中止
    signal?.throwIfAborted();

    await processChunk(chunk);
  }

  return 'completed';
}

const controller = new AbortController();

processLargeFile(bigFile, controller.signal)
  .then(result => console.log(result))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('处理被取消');
    }
  });

// 用户可以随时取消
cancelButton.onclick = () => controller.abort();

解决的问题

  • 让长时间运行的操作可以被中断
  • 统一的取消接口
  • 提高代码的可维护性

7. Promise 包装

为不支持 AbortSignal 的 API 添加取消功能:

javascript 复制代码
function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(resolve, ms);

    signal?.addEventListener('abort', () => {
      clearTimeout(timeoutId);
      reject(signal.reason);
    });
  });
}

const controller = new AbortController();

sleep(5000, controller.signal)
  .then(() => console.log('完成'))
  .catch(err => console.log('被中止'));

// 2秒后取消
setTimeout(() => controller.abort(), 2000);

解决的问题

  • 为旧 API 添加现代取消机制
  • 统一异步操作的取消接口
  • 提高代码一致性

总结

AbortSignal 和 AbortController 提供了一种标准、统一的方式来取消异步操作。它们的主要优势包括:

  1. 标准化:Web 平台统一的取消机制
  2. 灵活性:支持多种使用场景和组合方式
  3. 易用性:API 简单直观,易于理解和使用
  4. 可组合:可以组合多个 signal,支持复杂的控制流程
  5. 防止内存泄漏:帮助清理不再需要的异步操作

在现代 Web 应用开发中,合理使用 Abort API 可以显著提高应用的性能、响应性和用户体验。

参考资料

相关推荐
EnCi Zheng8 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen12 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技13 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人24 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实24 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha35 分钟前
三目运算符
linux·服务器·前端
晓晨的博客42 分钟前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
GISer_Jing1 小时前
AI全栈转型_TS后端学习路线
前端·人工智能·后端·学习