找回密码
立即注册
搜索
发回帖 发新帖

4972

积分

0

好友

638

主题
发表于 2026-8-19 20:34:47 | 查看: 145| 回复: 0

最近我在研究 JavaScript 的异步编程时,碰到两个挺有意思的问题。

  • 第一个问题是:一个 Promise 一旦创建了,任务跑到一半,我后悔了,能不能把它取消?
  • 第二个问题是:一个异步任务要执行很久,我总不能让用户盯着一个“加载中”干等吧?能不能告诉他,现在已经执行到 30%、50%、80% 了?

这两个问题看起来简单,其实正好戳中了 JavaScript 期约设计里的两个重要边界:Promise 擅长表达“最终结果”,却不负责表达“取消”和“过程”。

如果把一个异步任务比作点外卖,那么 Promise 就像外卖平台给你的订单状态通知:“已接单”“配送完成”“配送失败”。但问题来了,骑手已经出发了,你突然说:“我不要了,能不能让骑手马上回去?”或者你点了一份特别复杂的大餐,厨房要做十分钟,你希望平台告诉你:“现在做到 30% 了,再等五分钟。”

这时候,单纯的 Promise 就有点不够用了。今天我们就沿着这两个问题,把 JavaScript 的期约扩展能力彻底聊明白。

先搞清楚:Promise 到底解决了什么问题?

在 JavaScript 早期,处理异步任务并不是一件特别优雅的事情。回调套回调,代码很容易变成“回调地狱”。后来 Promise 出现了,我们可以把异步任务包装成一个 Promise:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("任务完成");
  }, 3000);
});

promise.then(result => {
  console.log(result);
}).catch(error => {
  console.error(error);
});

它非常漂亮。

  • 任务成功了,调用 resolve()。
  • 任务失败了,调用 reject()。

消费者通过 then()、catch()、finally() 处理最终结果。于是问题来了:Promise 能不能取消?

答案是 Promise 本身没有提供取消机制。这句话非常重要,很多刚接触 Promise 的同学会产生一个误区:“既然 Promise 是一个异步任务,那我把 Promise 设置成 rejected,不就相当于取消了吗?”

还真不是。

比如:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("任务仍然执行了");
    resolve("完成");
  }, 5000);
});

promise.catch(error => {
  console.log(error);
});

你无法通过 promise.reject() 这种方式把它取消,因为 Promise 的本质是一个状态容器,而不是一个“任务控制器”。

它主要描述的是:这个异步操作最终是成功、失败,还是仍然处于等待状态。而不是这个异步操作现在要不要继续执行。这两个概念一定要分开。

为什么 Promise 不能直接取消?

我们来看一个非常生活化的例子。假设我去餐厅点了一份牛肉面,服务员给了我一张取餐单,这张单子上面最终只有三种结果:

Promise 状态 牛肉面订单
pending 厨房正在做
fulfilled 牛肉面做好了
rejected 厨房说做不了了

Promise 就像这张订单,它负责告诉我最后到底发生了什么。但是,如果我突然说“我不吃了”,这个动作实际上是在控制厨房的生产过程,订单本身并没有办法直接让厨师停下来。这就是 Promise 和取消机制之间的区别。

  • Promise 是:结果通知机制。
  • 取消机制是:任务控制机制。

所以,如果我们想取消异步操作,就需要额外设计一个“控制器”。

现代 JavaScript 最常见的解决方案,就是 AbortController。

AbortController:给异步任务装一个刹车

AbortController 可以理解成异步任务的“刹车”。最典型的场景就是取消网络请求,例如:

const controller = new AbortController();

fetch("/api/user", {
  signal: controller.signal
})
.then(response => response.json())
.then(data => {
  console.log(data);
})
.catch(error => {
  console.log("请求结束:", error.name);
});

setTimeout(() => {
  controller.abort();
}, 1000);

这里出现了三个角色:

AbortController
↓
signal
↓
fetch()
  • AbortController 负责发出“取消”命令。
  • signal 负责把这个命令传递给异步任务。
  • fetch() 负责监听这个信号。

当我们调用 controller.abort(),就相当于对任务说:“别做了,赶紧停。”

如果请求支持 AbortSignal,那么它就可以响应取消,例如:

async function requestData() {
  const controller = new AbortController();

  setTimeout(() => {
    controller.abort();
  }, 1000);

  try {
    const response = await fetch("/api/data", {
      signal: controller.signal
    });

    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("请求被取消");
    } else {
      console.log("请求失败");
    }
  }
}

这里有一个非常值得面试时说出来的细节:AbortController 不是把 Promise 从世界上“删除”了。它真正做的是给底层异步操作发送取消信号,最终 Promise 可能以拒绝结束。

所以,取消异步操作 ≠ 取消 Promise。更准确地说,是通过取消信号,让异步操作停止,并让相关 Promise 以取消导致的异常结束。

真正重要的问题:并不是所有异步任务都能被取消

这点非常容易被忽略。假设我们自己写了一个异步函数:

function doSomething() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("任务执行完成");
      resolve();
    }, 5000);
  });
}

你调用 const promise = doSomething(),这时候即使你不再使用 promise,里面的 setTimeout() 依然会继续执行。这就是一个关键概念:Promise 的消费者停止等待,不代表生产者停止工作。

比如:

const promise = doSomething();

setTimeout(() => {
  // 我不关心 promise 了
}, 1000);

这里最多只能说“我不再关心这个结果”,并不能说“任务已经停止”。所以,如果我们自己设计一个可取消任务,就应该显式支持 AbortSignal。

例如:

function delay(ms, signal) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      resolve("完成");
    }, ms);

    if (signal) {
      signal.addEventListener("abort", () => {
        clearTimeout(timer);
        reject(new Error("任务取消"));
      });
    }
  });
}

调用:

const controller = new AbortController();

delay(5000, controller.signal)
.then(() => {
    console.log("完成");
})
.catch(error => {
    console.log(error.message);
});

setTimeout(() => {
    controller.abort();
}, 1000);

这时候,任务真正实现了“取消”。为什么?因为我们不仅发出了取消信号,还在任务内部真正响应了这个信号。这才是完整的取消机制。

第二个问题:Promise 为什么不能通知进度?

取消解决了,接下来我们再看第二个问题。假设现在有一个文件上传任务。

  • 文件有 2GB。
  • 网络速度又比较慢。

用户点击上传:

  • 然后页面上显示:正在上传……
  • 一分钟过去了,还是:正在上传……
  • 五分钟过去了,还是:正在上传……

这时候用户心里只有一个问题:到底上传到哪里了?

Promise 在这里又遇到了一个天然限制。Promise 通常只能告诉你:

pending
↓
fulfilled / rejected

它擅长表达最终结果,但是它没有一个标准的 30%、50%、80% 这样的“中间状态”。

Promise 像终点通知,进度像沿途路标

这个区别特别好理解。假设我要从北京开车去上海,Promise 就像导航最后告诉我:“到了。”或者“没到。”

但如果我要知道“现在到哪儿了?”,那就需要另一套机制。所以我们可以把异步系统拆成两个维度:

能力 解决的问题
Promise 最终结果是什么
AbortSignal 任务要不要继续
Progress 当前执行到哪里
Event 中间发生了什么
Async Iterator 持续产生数据

这其实就是现代异步编程非常重要的一种思想:不要让一个抽象承担所有职责。Promise 不需要强行变成万能异步对象。

最经典的方案:回调通知进度

如果我们自己实现一个任务,可以增加一个 onProgress 参数,比如:

function downloadFile(onProgress) {
  let progress = 0;

  return new Promise(resolve => {
    const timer = setInterval(() => {
      progress += 10;

      if (onProgress) {
        onProgress(progress);
      }

      if (progress >= 100) {
        clearInterval(timer);
        resolve("下载完成");
      }
    }, 500);
  });
}

调用:

downloadFile(progress => {
  console.log('当前进度:${progress}%');
}).then(result => {
  console.log(result);
});

输出可能是:

  • 当前进度:10%
  • 当前进度:20%
  • 当前进度:30%
  • 当前进度:40% ……
  • 当前进度:100%
  • 下载完成

这就非常舒服了。

  • Promise 负责:最后告诉我任务成功还是失败。
  • onProgress 负责:不断告诉我任务现在走到哪里了。

两者各司其职。

AbortController + Progress:取消和进度一起解决

真正实用的异步任务,往往既需要取消,也需要进度。我们可以把它们组合起来。

function longTask({ signal, onProgress }) {
  return new Promise((resolve, reject) => {
    let progress = 0;

    const timer = setInterval(() => {
      progress += 10;
      onProgress?.(progress);

      if (progress >= 100) {
        clearInterval(timer);
        resolve("任务完成");
      }
    }, 500);

    signal?.addEventListener("abort", () => {
      clearInterval(timer);
      reject(new Error("任务已取消"));
    });
  });
}

使用:

const controller = new AbortController();

longTask({
  signal: controller.signal,
  onProgress(progress) {
    console.log(`任务进度: ${progress}%`);
  }
})
.then(result => {
  console.log(result);
})
.catch(error => {
  console.log(error.message);
});

setTimeout(() => {
  controller.abort();
}, 1800);

执行过程中可能出现:

  • 任务进度:10%
  • 任务进度:20%
  • 任务进度:30%
  • 任务已取消

这时候,一个比较完整的异步任务模型就出现了:

异步任务
├── Progress:当前进度
├── AbortSignal:是否取消
└── Promise:最终成功 / 失败

这才是比较成熟的设计思路。

再进一步:用事件实现进度通知

如果一个任务需要通知的不只是进度,而是很多种状态,那么单纯传一个 onProgress 回调可能就不够优雅。这时候可以使用事件,例如:

class Task extends EventTarget {
  start() {
    let progress = 0;

    const timer = setInterval(() => {
      progress += 20;

      this.dispatchEvent(
        new CustomEvent("progress", { detail: progress })
      );

      if (progress >= 100) {
        clearInterval(timer);
        this.dispatchEvent(new Event("complete"));
      }
    }, 500);
  }
}

使用:

const task = new Task();

task.addEventListener("progress", event => {
  console.log('进度: ${event.detail}%');
});

task.addEventListener("complete", () => {
  console.log("任务完成");
});

task.start();

这种方式最大的好处是:生产者和消费者之间的耦合更低。任务只负责“发生了 progress 事件”,至于谁监听、谁处理,由消费者决定。

还有一个更现代的思路:Async Iterator

如果任务不是简单的“0% 到 100%”,而是会持续产生很多数据,那么 Async Iterator 也非常有价值。例如:

async function* progressTask() {
  for (let i = 10; i <= 100; i += 10) {
    await new Promise(resolve => setTimeout(resolve, 500));
    yield i;
  }
}

使用:

for await (const progress of progressTask()) {
  console.log('当前进度: ${progress}%');
}

它的思维方式和 Promise 不一样。

  • Promise 是:给我一个最终结果。
  • Async Iterator 是:以后会不断给我结果,一个接一个。

所以,如果说 Promise 是“最终成绩单”,那么 Async Iterator 就更像“直播比分”。

期约扩展的核心,其实是三个问题

讲到这里,我们可以把今天的内容浓缩成三个问题。

第一:最终结果是什么?用 Promise。

第二:任务还要不要继续?用 AbortController / AbortSignal。

第三:任务现在进行到哪里了?用 Progress、事件或者 Async Iterator。

这三个问题不要混在一起。

问题 推荐机制 核心职责
最终结果 Promise 成功、失败
取消任务 AbortController 发出取消信号
响应取消 AbortSignal 监听取消
进度通知 回调 / Event 通知执行进度
持续产生数据 Async Iterator 持续消费异步数据

如果你在面试中被问:“Promise 怎么取消?”

千万不要直接回答:“Promise 可以取消。”

更准确的回答应该是:

Promise 本身没有原生取消机制。如果底层异步操作支持取消,可以通过 AbortController 和 AbortSignal 协作实现取消。对于自定义任务,则需要在任务内部主动监听 AbortSignal,并执行真正的资源清理。

如果继续追问:“Promise 怎么通知进度?”

可以回答:

Promise 本身主要描述最终状态,不提供通用的进度通知机制。可以通过回调、事件、Async Iterator 等机制额外传递中间进度。

这两个回答,基本就把 JavaScript 期约的设计边界说清楚了。

最后再理解一次:Promise 不是万能胶

以前我刚学习 Promise 的时候,总觉得 Promise 好像什么都能干。后来才发现,Promise 更像一个非常专业的“快递单”,它特别擅长告诉你:快递最后到了没有?

但你让它负责“快递员现在在哪里”,它就不太擅长了。

你再问“快递员能不能取消配送”,这又是另外一套机制。

所以现代 JavaScript 的异步编程,并不是不断给 Promise 塞功能,而是在不同抽象之间进行组合:

  • Promise 管最终结果,
  • AbortSignal 管取消,
  • 事件或回调管进度,
  • Async Iterator 管持续的数据流。

这其实也是 JavaScript 异步设计越来越成熟的一个表现。

我们写异步代码的时候,真正应该思考的不是“Promise 还能不能继续加功能?”,而是“我的异步任务到底需要表达什么信息?”

  • 是最终结果?
  • 是取消?
  • 是进度?
  • 还是连续不断的数据?

把问题拆开,方案反而会变得简单。

好了,这一期关于 JavaScript 期约扩展就聊到这里。如果下一次面试官问你:“Promise 能取消吗?能不能报告任务进度?”

别急着背 API,先想想今天那个外卖的故事:

  • Promise 是订单结果,
  • AbortController 是取消按钮,
  • 而 Progress 是一路上的导航路标。

三者各干各的活,整个异步系统,反而更稳、更清晰。更多 JavaScript 异步编程相关内容,也欢迎到 云栈社区 一起交流。




上一篇:用 Discord CLI 把用户反馈喂给 Claude Code,加速迭代到 PMF
下一篇:腾讯朱雀开源AI安全检测平台:Agent红队实测与提示词泄露防护
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-5 08:02 , Processed in 0.090528 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表