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 可以显著提高应用的性能、响应性和用户体验。

参考资料

相关推荐
Laravel技术社区9 分钟前
用PHP8实现斗地主游戏,实现三带一,三带二,四带二,顺子,王炸功能(第二集)
前端·游戏·php
m0_738120721 小时前
应急响应——知攻善防Web-3靶机详细教程
服务器·前端·网络·安全·web安全·php
程序员爱钓鱼8 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
PineappleCoder8 小时前
工程化必备!SVG 雪碧图的最佳实践:ID 引用 + 缓存友好,无需手动算坐标
前端·性能优化
JIngJaneIL9 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码9 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web
澄江静如练_9 小时前
列表渲染(v-for)
前端·javascript·vue.js
JustHappy10 小时前
「chrome extensions🛠️」我写了一个超级简单的浏览器插件Vue开发模板
前端·javascript·github
Loo国昌10 小时前
Vue 3 前端工程化:架构、核心原理与生产实践
前端·vue.js·架构