当前位置:首页 > 技术文章 > 正文内容

如何取消一个已经开始的 JavaScript Promise

zonemu1个月前 (09-12)技术文章22

在现代前端开发中,Promise 和 async/await 已经成为处理异步操作的基石。然而,一个常见的棘手问题是:如何取消一个已经开始的 Promise?

比如,用户发起一个数据请求,但在请求完成前又导航到了其他页面;或者用户在一个搜索框中快速输入,我们需要取消前一次的搜索请求,只保留最后一次。在这些场景下,取消一个进行中的 Promise 就显得至关重要。

核心问题:为什么 Promise 本身不可取消?

首先,我们需要理解 Promise 的核心设计理念。一个 Promise 代表一个异步操作的最终结果。它的状态一旦从 pending(进行中)变为 fulfilled(已成功)或 rejected(已失败),就永远不会再改变。

Promise 本身不提供取消机制,原因如下:

  1. 状态不可逆:这是 Promise 的核心规范。一旦状态改变,就形成了一个确定的、不可变的结果。
  2. 单一责任:Promise 的职责是传递价值和状态,而不是控制异步操作本身的执行流程。发起异步操作的函数(如 fetch)才是执行者。

打个比方:你寄出了一封信(发起了一个 Promise),你不能在信件投递过程中把它神奇地从邮政系统里撤回来。你能做的,是在信件送达时(Promise 完成时),选择忽略它

我们的目标,就是实现这种“忽略”机制,并尽可能地通知底层的异步操作停止工作,以节省资源。

AbortController

AbortController 是目前实现 Promise 取消的最佳实践和标准方案。它最初是为取消 fetch 请求而设计的,但其通用性使其可以与任何异步操作集成。

AbortController 的工作方式

  1. 创建一个 AbortController 实例。
  2. controller.signal:这是一个 AbortSignal 对象,可以传递给需要支持取消的异步函数(如 fetch)。
  3. controller.abort():调用此方法来发出“中止”信号。
  4. abort() 被调用时,signal 会通知所有监听它的异步操作。对于 fetch 来说,它会自动中止网络请求并让 Promise reject 一个名为 AbortError 的错误。

1. 与 fetch配合使用

这是 AbortController 最常见的用法。

// 创建一个控制器
const controller = new AbortController();
const signal = controller.signal;

console.log("开始请求数据...");

fetch('https://api.example.com/data', { signal })
  .then(response => response.json())
  .then(data => {
    console.log("数据获取成功:", data);
  })
  .catch(err => {
    // 检查错误是否是由于中止操作引起的
    if (err.name === 'AbortError') {
      console.log('Fetch 请求已被用户中止。');
    } else {
      console.error('Fetch 错误:', err);
    }
  });

// 比如,在 500 毫秒后用户决定取消请求
setTimeout(() => {
  console.log("正在中止请求...");
  controller.abort();
}, 500);

async/await 语法中同样清晰:

async function fetchData() {
  const controller = new AbortController();
  const signal = controller.signal;

  // 如果 3 秒内用户点击了取消按钮,就中止请求
  const cancelButton = document.getElementById('cancel-btn');
  cancelButton.onclick = () => controller.abort();

  try {
    const response = await fetch('...', { signal });
    const data = await response.json();
    console.log(data);
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('请求已取消');
    } else {
      console.error('请求失败', err);
    }
  }
}

2. 在自定义 Promise 中使用 AbortController

你也可以让你自己的异步函数支持 AbortSignal

原理

  • 你的函数需要接收 signal 作为参数。
  • 在异步操作的关键节点,检查 signal.aborted 属性。如果为 true,则提前退出。
  • 使用 signal.addEventListener('abort', ...) 来注册清理逻辑(如清除定时器)。
function longRunningTask(signal) {
  return new Promise((resolve, reject) => {
    // 如果信号在任务开始前就已经被中止,则直接拒绝
    if (signal.aborted) {
      return reject(new DOMException('任务已中止', 'AbortError'));
    }

    console.log("耗时任务开始...");

    const timeoutId = setTimeout(() => {
      console.log("耗时任务完成!");
      resolve("成功!");
    }, 5000);

    // 监听 abort 事件,用于清理工作
    signal.addEventListener('abort', () => {
      clearTimeout(timeoutId); // 清除定时器,真正中止了操作
      console.log("定时器已被清除,任务已中止。");
      reject(new DOMException('任务已中止', 'AbortError'));
    });
  });
}

// --- 使用示例 ---
const controller = new AbortController();

longRunningTask(controller.signal)
  .then(console.log)
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('捕获到 AbortError,任务被成功取消。');
    }
  });

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

优点

  • 官方标准:是 W3C 和 WHATWG 定义的标准 API。
  • 真正中止底层操作fetch 会中止网络连接,自定义函数也可以通过它来清理资源(如清除定时器),避免了不必要的浪费。
  • 语义清晰:通过专门的 AbortError 来区分“取消”和“其他错误”,代码更健壮。
  • 组合性强:一个 AbortSignal 可以传递给多个 Promise,实现批量取消。

虽然 Promise 本身的核心设计使其不可变,但通过 AbortController 这一强大的模式,我们已经可以非常有效地控制和终止异步流程,编写出更健壮、更高效的应用程序。

相关文章

财务主管花了一周时间自制费用报销管理系统,是我见过最好用的

公司的费用报销又多又乱,一不小心就出错!头疼,财务主管花了一周时间自制费用报销管理台账,分类统计,重复报销还能自动提醒,真的少了很多麻烦!费用报销是财务日常工作必会面对的,各种票据太多太乱,搞的很烦,...

细数5款国外热门Linux发行版(linux发行版排名网站)

Linux系统已经与我们的生活息息相关,当你用Android手机浏览这篇文章时,你就已经在使用Linux系统。当然作为编程开发最热门的系统,他还有很多专注于开发使用的版本。Fedora热门入门推荐,一...

Vue3快速入门(vue3快速上手)

  1.核心语法  1. 1选项式和组合式的区别  Vue2的API设计是Options(选项)风格的。  Vue3的API设计是Composition(组合)风格的。  Options类型的 API...

傻瓜式DEVOPS实践手册——Gitlab部署

GitLab是一款开放源代码的DevOps平台,用于实现从项目规划、源代码管理、CI/CD到监控和安全性的全方位集成。GitLab主要用于版本控制、协同开发、持续集成/持续部署 (CI/CD)、自动化...

基于Docker构建安装Git/GitLab,以及制作springboot工程镜像

今天给大家分享的是《领先的开源自动化服务器Jenkins的应用实战》之基于Docker安装构建Git/GitLab版本控制与代码云存储的场所;使用Git管理项目,springboot工程制作镜像知识体...

「云原生」Containerd ctr,crictl 和 nerdctl 命令介绍与实战操作

一、概述作为接替Docker运行时的Containerd在早在Kubernetes1.7时就能直接与Kubelet集成使用,只是大部分时候我们因熟悉Docker,在部署集群时采用了默认的dockers...