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

参考资料

相关推荐
Tomoon4 小时前
前端开发者的全栈逆袭
前端
Filotimo_5 小时前
2.CSS3.(3).html
前端·css3
whyfail5 小时前
React v19.2版本
前端·javascript·react.js
慧慧吖@5 小时前
react基础
前端·javascript·react.js
浪裡遊5 小时前
MUI组件库与主题系统全面指南
开发语言·前端·javascript·vue.js·react.js·前端框架·node.js
DiXinWang5 小时前
关闭谷歌浏览器提示“若要接收后续 Google Chrome 更新,您需使用 Windows 10 或更高版本”的方法
前端·chrome
CoderYanger6 小时前
前端基础——HTML练习项目:填写简历信息
前端·css·职场和发展·html
muyouking116 小时前
深入理解 HTML `<label>` 的 `for` 属性:提升表单可访问性与用户体验
前端·html·ux