PhysChen.com
主页
物理
笔记 科普 研究
教学
IB 课程
编程
笔记 项目
随笔
所感 所思
摄影
Shenzhen Portrait Cats Others Wuhan Japan
关于
主页
物理
笔记 科普 研究
编程
笔记 项目
摄影
Shenzhen Portrait Cats Others Wuhan Japan
教学
IB 课程
随笔
所感 所思
关于
文章目录
    JavaScript 4. 异步编程与事件循环 陈华的个人主页

    文章信息

    • 标题: JavaScript 4. 异步编程与事件循环
    • 发布时间: 2026 年 7 月 19 日
    • 来源: https://physchen.com/zh-Hans/programming/notes/javascript-asynchronous-programming/
    • 摘要: 介绍浏览器中的任务与微任务调度、回调函数、Promise、async/await、并发组合以及异步操作的取消机制,建立准确的 JavaScript 异步执行模型。

    目录

      JavaScript 4. 异步编程与事件循环

      发布于 2026 年 7 月 19 日
      • JavaScript 基础
      • JavaScript
      • 异步编程

      JavaScript 程序经常需要等待网络响应⁠、定时器⁠、用户交互⁠、文件读写或其他外部事件⁠。如果程序在等待期间持续占用执行线程⁠,页面将无法及时响应输入⁠,服务器也难以同时处理其他工作⁠。

      异步编程的目的不是让所有代码自动并行执行⁠,而是让程序能够在某项工作尚未完成时⁠,把执行机会交给其他任务⁠,并在结果可用后继续处理⁠。

      理解 JavaScript 异步编程⁠,需要区分三个层次⁠:

      1. ECMAScript 语言层定义函数⁠、Promise⁠、async⁠、await 和 Job 等语义⁠;
      2. 宿主环境提供定时器⁠、网络请求⁠、用户事件和事件循环⁠;
      3. 运行时实现决定线程⁠、I/O⁠、操作系统接口和内部优化方式⁠。

      本章以浏览器环境为主要背景⁠。Node.js 也使用事件驱动模型⁠,但其事件循环阶段⁠、process.nextTick() 和部分 I/O 调度规则与浏览器不同⁠,不应将两者完全等同⁠。

      4.1 同步执行⁠、并发与并行

      4.1.1 同步执行

      同步代码按照当前控制流依次执行⁠。前一条语句没有完成时⁠,后一条语句不能开始⁠:

      console.log("first");
      console.log("second");
      console.log("third");

      输出顺序固定为⁠:

      first
      second
      third

      函数调用会建立相应的执行上下文⁠。函数返回后⁠,调用位置才会继续执行⁠:

      function calculate() {
        return 1 + 2;
      }
      
      const result = calculate();
      
      console.log(result); // 3

      如果同步代码长时间占用浏览器主线程⁠,用户输入⁠、布局⁠、绘制和其他需要该线程完成的工作都会受到影响⁠。

      4.1.2 并发与并行

      并发表示多个任务在一段时间内共同推进⁠。它们不一定在同一时刻执行⁠。

      并行表示多个任务在同一时刻由不同执行资源运行⁠,例如多个 CPU 核心分别执行不同线程⁠。

      浏览器主线程上的普通 JavaScript 回调通常一次只执行一个⁠。异步 I/O 可以在 JavaScript 代码不占用主线程的情况下由宿主环境或操作系统推进⁠,因此多个操作能够并发进行⁠。

      这并不意味着下面的 CPU 密集型循环会因为写在 async 函数中而自动移到后台⁠:

      async function calculateLargeSum() {
        let sum = 0;
      
        for (let i = 0; i < 1_000_000_000; i += 1) {
          sum += i;
        }
      
        return sum;
      }

      在遇到能够挂起函数的 await 之前⁠,这段循环仍然同步占用当前线程⁠。需要把大量计算移出浏览器主线程时⁠,应考虑 Web Worker⁠,而不是仅仅添加 async⁠。

      4.1.3 JavaScript 并非在所有环境中只有一个线程

      “⁠JavaScript 是单线程语言⁠”是一种便于入门的概括⁠,但不够完整⁠。

      在一个典型的浏览器 Window 上下文中⁠,脚本回调通常在负责该页面的事件循环线程上依次执行⁠。不过⁠,浏览器还可以创建⁠:

      • Dedicated Worker⁠;
      • Shared Worker⁠;
      • Service Worker⁠;
      • Worklet⁠;
      • 其他由浏览器和操作系统管理的并行工作⁠。

      不同 Worker 拥有各自的全局环境和事件循环⁠,并可以与页面线程交换消息⁠。ECMAScript 还定义了 Agent⁠、共享内存和原子操作等机制⁠。

      因此⁠,更准确的表述是⁠:在同一个 JavaScript Agent 中⁠,普通 ECMAScript 执行上下文不会同时并行执行⁠;浏览器可以通过多个 Agent 和宿主线程实现并行工作⁠。

      4.2 浏览器事件循环

      事件循环负责协调任务⁠、微任务以及浏览器的其他工作⁠。它不是 ECMAScript 单独定义的机制⁠,而是由 HTML 等宿主标准规定⁠。

      4.2.1 执行上下文栈

      ECMAScript 使用执行上下文栈描述当前正在运行的代码关系⁠。函数调用时⁠,相应执行上下文进入栈⁠;函数结束时退出⁠:

      function second() {
        console.log("second");
      }
      
      function first() {
        console.log("first start");
        second();
        console.log("first end");
      }
      
      first();

      输出为⁠:

      first start
      second
      first end

      “⁠调用栈⁠”是对此执行关系的常用称呼⁠。引擎可以对函数调用进行内联等优化⁠,但必须保持规范要求的可观察行为⁠。

      4.2.2 任务

      浏览器会把脚本执行⁠、定时器回调⁠、用户交互和许多宿主操作安排为任务(⁠Task⁠)⁠。

      常见任务来源包括⁠:

      • 初始脚本执行⁠;
      • 定时器⁠;
      • 用户交互事件⁠;
      • 网络和其他宿主事件⁠;
      • 消息通道⁠;
      • 渲染相关工作⁠。

      “⁠宏任务⁠”(⁠Macrotask⁠)是教学和工程讨论中常见的非正式术语⁠,通常用于和微任务相对照⁠。HTML 标准使用的正式术语是 Task⁠。

      浏览器并不是只维护一个全局先进先出的“⁠宏任务队列⁠”⁠。事件循环可以关联多个任务队列⁠,任务还具有不同的任务来源⁠。浏览器会从某个具有可运行任务的队列中选择任务⁠;同一任务来源中的相对顺序必须满足相应规范要求⁠,但不同来源之间不应假定存在一个简单⁠、固定的全局顺序⁠。

      4.2.3 微任务

      事件循环还维护微任务队列⁠。常见微任务来源包括⁠:

      • Promise 反应回调⁠,即 .then()⁠、.catch() 和 .finally() 产生的后续工作⁠;
      • queueMicrotask()⁠;
      • MutationObserver 的通知过程⁠。

      ECMAScript 将 Promise 后续处理描述为 Job⁠,宿主环境负责把相关 Job 接入自己的微任务调度机制⁠。

      下面的代码展示任务与微任务的基本顺序⁠:

      console.log("script start");
      
      setTimeout(() => {
        console.log("timer");
      }, 0);
      
      Promise.resolve().then(() => {
        console.log("promise reaction");
      });
      
      queueMicrotask(() => {
        console.log("queued microtask");
      });
      
      console.log("script end");

      通常输出⁠:

      script start
      script end
      promise reaction
      queued microtask
      timer

      原因是⁠:

      1. 当前脚本作为任务开始执行⁠;
      2. 定时器登记后等待未来任务调度⁠;
      3. Promise 回调和 queueMicrotask() 回调进入微任务队列⁠;
      4. 当前同步脚本结束⁠;
      5. 浏览器执行微任务检查点⁠,依次清空微任务队列⁠;
      6. 定时器满足条件后⁠,其回调才有机会作为后续任务执行⁠。

      4.2.4 微任务检查点

      在一次任务结束后⁠,浏览器通常会执行微任务检查点⁠。检查点会持续处理微任务⁠,直到微任务队列为空⁠。

      如果微任务在执行过程中继续加入新的微任务⁠,新加入的微任务也会在当前检查点继续执行⁠:

      queueMicrotask(() => {
        console.log("microtask 1");
      
        queueMicrotask(() => {
          console.log("microtask 3");
        });
      });
      
      queueMicrotask(() => {
        console.log("microtask 2");
      });

      输出为⁠:

      microtask 1
      microtask 2
      microtask 3

      不断递归创建微任务可能长期推迟后续任务和渲染⁠:

      function repeatMicrotask() {
        queueMicrotask(repeatMicrotask);
      }
      
      // 不要执行:可能使页面无法继续处理其他任务。
      // repeatMicrotask();

      微任务适合处理短小⁠、必须紧随当前任务完成的状态收尾工作⁠,不适合承载长时间计算⁠。

      4.2.5 渲染机会

      常见教学模型会写成⁠:

      执行一个任务
      → 清空微任务
      → 页面渲染
      → 执行下一个任务

      这个模型有助于理解基本顺序⁠,但不能理解为每个任务结束后浏览器都必定绘制一帧⁠。

      浏览器只会在存在渲染机会且确有更新需要时执行相关工作⁠。刷新率⁠、页面可见性⁠、设备性能和浏览器调度策略都会影响实际渲染时机⁠。

      因此⁠,只能稳定地认为⁠:

      1. 一个任务执行期间不会被另一个普通任务任意插入⁠;
      2. 任务结束后会进行适当的微任务检查点⁠;
      3. 浏览器可能在合适的渲染机会更新页面⁠;
      4. 之后事件循环继续选择可运行任务⁠。

      4.2.6 定时器不是精确计时器

      setTimeout() 的延迟表示回调在至少经过指定时间后才有资格被调度⁠,并不保证届时立即执行⁠:

      const start = performance.now();
      
      setTimeout(() => {
        const elapsed = performance.now() - start;
        console.log(elapsed);
      }, 100);

      输出通常不小于约 100 ms⁠,但可能明显更大⁠。延迟来源包括⁠:

      • 当前任务尚未结束⁠;
      • 微任务队列持续有工作⁠;
      • 其他任务正在执行⁠;
      • 页面处于后台⁠;
      • 浏览器节流⁠;
      • 操作系统调度⁠。

      浏览器定时器还具有嵌套层级限制⁠。连续嵌套超过五层时⁠,小于 4 ms 的请求延迟会被限制为至少 4 ms⁠。实际延迟仍然可能更长⁠。

      定时器的详细基础用法已在第二章介绍⁠。本章只关心它在事件循环中的任务调度位置⁠。

      4.3 回调函数

      回调函数是由另一段代码在适当时机调用的函数⁠。回调可以同步执行⁠,也可以异步执行⁠。

      4.3.1 同步回调

      数组方法中的回调通常同步执行⁠:

      const values = [1, 2, 3];
      
      const doubled = values.map((value) => {
        return value * 2;
      });
      
      console.log(doubled); // [2, 4, 6]

      map() 返回前⁠,所有回调已经执行完成⁠。

      4.3.2 异步回调

      事件监听器和定时器回调由宿主环境在未来调度⁠:

      button.addEventListener("click", () => {
        console.log("button clicked");
      });
      
      setTimeout(() => {
        console.log("timer completed");
      }, 1000);

      调用注册函数时⁠,回调不会立即执行⁠。调用方只是在宿主环境中登记后续行为⁠。

      4.3.3 错误优先回调

      错误优先回调(⁠Error-First Callback⁠)是 Node.js 传统 API 中常见的约定⁠,不是 ECMAScript 语言规则⁠,也不是所有浏览器回调的统一形式⁠。

      典型形式为⁠:

      function readData(callback) {
        const succeeded = true;
      
        setTimeout(() => {
          if (!succeeded) {
            callback(
              new Error("Failed to read data"),
            );
            return;
          }
      
          callback(null, {
            value: 42,
          });
        }, 100);
      }
      
      readData((error, data) => {
        if (error !== null) {
          console.error(error);
          return;
        }
      
        console.log(data);
      });

      约定通常是⁠:

      • 第一个参数表示错误⁠;
      • 成功时错误参数为 null 或 undefined⁠;
      • 后续参数表示成功结果⁠。

      是否采用这种约定⁠,取决于具体 API 文档⁠。

      4.3.4 控制反转

      把回调交给外部 API 后⁠,调用者将“⁠何时调用⁠、调用几次⁠、以什么参数调用⁠”的一部分控制权交给了 API⁠:

      thirdPartyOperation((result) => {
        console.log(result);
      });

      如果 API 设计不可靠⁠,可能出现⁠:

      • 从不调用回调⁠;
      • 重复调用回调⁠;
      • 同步和异步调用方式不一致⁠;
      • 传入错误的参数⁠;
      • 抛出无法被调用方预期处理的异常⁠。

      这类问题常被归入控制反转(⁠Inversion of Control⁠)风险⁠。

      Promise 通过标准化状态⁠、后续处理和错误传播⁠,降低了部分风险⁠。它可以保证一次 Promise 的最终结果只采用第一次有效的决议⁠,但不能自动保证底层外部操作会启动⁠、完成或支持取消⁠。

      4.3.5 回调嵌套

      多个相互依赖的异步操作容易形成深层嵌套⁠:

      getUser(userId, (userError, user) => {
        if (userError) {
          handleError(userError);
          return;
        }
      
        getOrders(user.id, (orderError, orders) => {
          if (orderError) {
            handleError(orderError);
            return;
          }
      
          saveSummary(orders, (saveError, result) => {
            if (saveError) {
              handleError(saveError);
              return;
            }
      
            console.log(result);
          });
        });
      });

      问题不只是缩进增加⁠,还包括⁠:

      • 成功路径和错误路径交织⁠;
      • 每一层都要决定是否继续⁠;
      • 资源清理容易分散⁠;
      • 重用和组合困难⁠;
      • 外层 try...catch 无法捕获未来任务中的异常⁠。

      这类结构通常称为回调地狱⁠。Promise 和 async/await 可以让控制流更接近普通顺序代码⁠。

      4.4 Promise

      Promise 表示一个延迟计算的最终结果⁠。这个结果可能来自异步操作⁠,也可能来自被统一包装的同步值⁠。

      4.4.1 三种状态

      Promise 具有三种互斥状态⁠:

      • pending⁠:待定⁠;
      • fulfilled⁠:已兑现⁠;
      • rejected⁠:已拒绝⁠。

      Promise 处于 fulfilled 或 rejected 时⁠,统称为 settled⁠,可译为“⁠已敲定⁠”⁠。

      const promise = new Promise((resolve, reject) => {
        const succeeded = true;
      
        if (succeeded) {
          resolve("completed");
        } else {
          reject(
            new Error("failed"),
          );
        }
      });

      Promise 的结果由内部状态管理⁠,不能通过普通属性直接修改⁠。

      4.4.2 settled 与 resolved 的区别

      “⁠已决议⁠”(⁠Resolved⁠)不完全等同于“⁠已兑现⁠”(⁠Fulfilled⁠)⁠。

      如果使用另一个仍然待定的 Promise 决议当前 Promise⁠,当前 Promise 会锁定并跟随另一个 Promise 的最终状态⁠,但此时仍可能保持 pending⁠:

      let resolveInner;
      
      const inner = new Promise((resolve) => {
        resolveInner = resolve;
      });
      
      const outer = new Promise((resolve) => {
        resolve(inner);
      });
      
      outer.then((value) => {
        console.log(value);
      });
      
      resolveInner("completed");

      outer 在采用 inner 的状态后已经 resolved⁠,但要等 inner 最终兑现后才 fulfilled⁠。

      因此⁠,不应把 resolve(value) 简单解释为“⁠立即把 Promise 改成 fulfilled⁠”⁠。如果传入 Promise 或 thenable⁠,决议过程会采用其最终状态⁠。

      4.4.3 状态只能采用一次

      用于决议 Promise 的函数只接受第一次有效操作⁠:

      const promise = new Promise((resolve, reject) => {
        resolve("first");
        reject(new Error("second"));
        resolve("third");
      });
      
      promise.then((value) => {
        console.log(value); // first
      });

      后续 resolve() 或 reject() 不会改变已经采用的结果⁠。

      不过⁠,底层工作可能仍然继续运行⁠。Promise 状态锁定并不会自动取消网络请求⁠、定时器或其他外部操作⁠。

      4.4.4 执行器同步运行

      传给 new Promise() 的执行器函数会立即同步调用⁠:

      console.log("before");
      
      const promise = new Promise((resolve) => {
        console.log("executor");
        resolve("result");
      });
      
      console.log("after");

      输出为⁠:

      before
      executor
      after

      Promise 并不会自动把执行器中的同步计算移入后台⁠。下面的代码仍会阻塞当前线程⁠:

      const promise = new Promise((resolve) => {
        let total = 0;
      
        for (let i = 0; i < 1_000_000_000; i += 1) {
          total += i;
        }
      
        resolve(total);
      });

      Promise 主要统一结果和后续控制流⁠,不负责创建并行线程⁠。

      4.4.5 执行器中的异常

      执行器同步抛出的异常会使 Promise 被拒绝⁠:

      const promise = new Promise(() => {
        throw new Error("executor failed");
      });
      
      promise.catch((error) => {
        console.error(error.message);
      });

      但执行器内部未来异步回调抛出的异常⁠,不会由 Promise 构造器自动捕获⁠:

      const promise = new Promise((resolve) => {
        setTimeout(() => {
          // 这不是 Promise 执行器同步调用期间的异常。
          // throw new Error("timer failed");
      
          resolve("completed");
        }, 0);
      });

      异步回调需要自行调用 reject()⁠,或者使用本身能够返回 Promise 的 API⁠。

      4.4.6 Promise 回调始终异步调度

      即使 Promise 已经兑现⁠,.then() 的回调也不会在当前同步代码中立即执行⁠:

      const promise = Promise.resolve("result");
      
      promise.then((value) => {
        console.log(value);
      });
      
      console.log("synchronous");

      输出为⁠:

      synchronous
      result

      Promise 反应会通过 Job 调度⁠,并由宿主接入微任务处理⁠。

      4.5 Promise 实例方法

      4.5.1 then

      .then() 用于注册兑现和拒绝处理器⁠:

      promise.then(
        (value) => {
          console.log("fulfilled:", value);
        },
        (reason) => {
          console.error("rejected:", reason);
        },
      );

      每次调用 .then() 都返回一个新的 Promise⁠:

      const first = Promise.resolve(1);
      const second = first.then(
        value => value + 1,
      );
      
      console.log(first === second); // false

      新 Promise 的最终结果取决于处理器如何结束⁠。

      4.5.2 返回普通值

      处理器返回普通值时⁠,链中的下一个 Promise 会以该值兑现⁠:

      Promise.resolve(2)
        .then((value) => {
          return value * 3;
        })
        .then((value) => {
          console.log(value); // 6
        });

      4.5.3 返回 Promise 或 thenable

      处理器返回 Promise 时⁠,链会等待并采用该 Promise 的最终结果⁠:

      function delayValue(value, delay) {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(value);
          }, delay);
        });
      }
      
      delayValue(2, 100)
        .then((value) => {
          return delayValue(
            value * 3,
            100,
          );
        })
        .then((value) => {
          console.log(value); // 6
        });

      Promise 决议过程也会采用符合 thenable 形式的对象⁠:

      const thenable = {
        then(resolve) {
          resolve("thenable result");
        },
      };
      
      Promise.resolve(thenable).then((value) => {
        console.log(value);
      });

      普通业务代码通常不需要手动实现 thenable⁠,但需要知道 Promise 会同化这类对象⁠。

      4.5.4 抛出异常

      处理器抛出异常时⁠,.then() 返回的新 Promise 会被拒绝⁠:

      Promise.resolve("data")
        .then(() => {
          throw new Error("processing failed");
        })
        .catch((error) => {
          console.error(error.message);
        });

      4.5.5 缺少处理器时的结果传递

      如果 .then() 缺少相应处理器⁠,结果会继续向后传递⁠:

      Promise.resolve(10)
        .then()
        .then((value) => {
          console.log(value); // 10
        });
      
      Promise.reject(
        new Error("failed"),
      )
        .then()
        .catch((error) => {
          console.error(error.message);
        });

      这使 Promise 链可以集中处理错误⁠。

      4.5.6 必须返回链中的 Promise

      常见错误是在处理器中启动异步操作⁠,却没有返回它⁠:

      function incorrect() {
        return fetchUser()
          .then((user) => {
            fetchOrders(user.id);
          })
          .then((orders) => {
            // orders 为 undefined
            console.log(orders);
          });
      }

      应返回内部 Promise⁠:

      function correct() {
        return fetchUser()
          .then((user) => {
            return fetchOrders(user.id);
          })
          .then((orders) => {
            console.log(orders);
            return orders;
          });
      }

      箭头函数的表达式体可以隐式返回⁠:

      function concise() {
        return fetchUser()
          .then(user => fetchOrders(user.id))
          .then(orders => processOrders(orders));
      }

      4.5.7 catch

      .catch(onRejected) 等价于⁠:

      promise.then(
        undefined,
        onRejected,
      );

      通常把 .catch() 放在链的后部⁠,可以捕获前面链路中未处理的拒绝和处理器抛出的异常⁠:

      fetchUser()
        .then(user => fetchOrders(user.id))
        .then(orders => processOrders(orders))
        .catch((error) => {
          console.error(error);
        });

      下面两种写法的捕获范围不同⁠:

      promise.then(
        (value) => {
          throw new Error("failed in onFulfilled");
        },
        (error) => {
          // 不能捕获同一次 then 的 onFulfilled 抛出的错误。
          console.error(error);
        },
      );

      同一个 .then() 的第二个参数只处理上游 Promise 的拒绝⁠,不能处理该 .then() 的兑现处理器自身抛出的错误⁠。

      后接 .catch() 可以捕获⁠:

      promise
        .then((value) => {
          throw new Error("failed in onFulfilled");
        })
        .catch((error) => {
          console.error(error);
        });

      除非需要分别处理兑现和拒绝⁠,工程代码通常更适合使用单参数 .then() 配合后续 .catch()⁠。

      4.5.8 catch 后的恢复

      如果 .catch() 正常返回一个值⁠,后续 Promise 会恢复为 fulfilled⁠:

      Promise.reject(
        new Error("request failed"),
      )
        .catch((error) => {
          console.error(error.message);
          return [];
        })
        .then((items) => {
          console.log(items); // []
        });

      如果错误无法在当前层处理⁠,应重新抛出⁠:

      promise.catch((error) => {
        if (error instanceof ExpectedError) {
          return fallbackValue;
        }
      
        throw error;
      });

      通用错误分类和异常重新抛出将在第八章详细讨论⁠。

      4.5.9 finally

      .finally() 注册无论 Promise 兑现还是拒绝都会执行的清理函数⁠:

      showLoading();
      
      loadData()
        .then((data) => {
          render(data);
        })
        .catch((error) => {
          showError(error);
        })
        .finally(() => {
          hideLoading();
        });

      finally 回调不接收上游结果参数⁠,因为它通常不应区分成功与失败⁠。

      当 finally 正常完成时⁠,上游结果会继续传递⁠:

      Promise.resolve(10)
        .finally(() => {
          console.log("cleanup");
        })
        .then((value) => {
          console.log(value); // 10
        });

      如果 finally 抛出异常或返回被拒绝的 Promise⁠,它会用新的拒绝结果覆盖原结果⁠:

      Promise.resolve("original")
        .finally(() => {
          throw new Error("cleanup failed");
        })
        .catch((error) => {
          console.error(error.message);
        });

      因此⁠,清理逻辑自身也需要保持可靠⁠。

      4.6 Promise 静态方法

      4.6.1 Promise.resolve

      Promise.resolve(value) 返回一个采用该值最终状态的 Promise⁠:

      Promise.resolve(42).then((value) => {
        console.log(value);
      });

      如果参数是当前构造器创建的 Promise⁠,通常会直接返回它⁠;如果是其他 Promise 或 thenable⁠,则会采用其状态⁠。

      4.6.2 Promise.reject

      Promise.reject(reason) 返回一个已经被拒绝的 Promise⁠:

      const promise = Promise.reject(
        new Error("failed"),
      );
      
      promise.catch((error) => {
        console.error(error.message);
      });

      拒绝原因可以是任意值⁠,但工程代码通常应使用 Error 或其子类⁠,以保留名称⁠、信息和堆栈⁠。

      4.6.3 Promise.all

      Promise.all(iterable) 适合所有任务都必须成功的并发组合⁠:

      const userPromise = fetchUser();
      const settingsPromise = fetchSettings();
      const messagesPromise = fetchMessages();
      
      Promise.all([
        userPromise,
        settingsPromise,
        messagesPromise,
      ]).then(([
        user,
        settings,
        messages,
      ]) => {
        console.log(
          user,
          settings,
          messages,
        );
      });

      主要语义是⁠:

      • 输入中的普通值也会通过 Promise 决议过程处理⁠;
      • 所有输入兑现后⁠,结果 Promise 才兑现⁠;
      • 结果数组顺序与输入顺序一致⁠,不取决于完成先后⁠;
      • 任一输入拒绝时⁠,结果 Promise 以首先观察到的拒绝原因拒绝⁠;
      • 其他底层操作不会因此自动取消⁠。

      “⁠快速失败⁠”只表示聚合 Promise 提前拒绝⁠,不表示其余网络请求或定时器停止⁠。

      const tasks = [
        delayValue("slow", 1000),
        Promise.reject(
          new Error("failed immediately"),
        ),
      ];
      
      Promise.all(tasks).catch((error) => {
        console.error(error.message);
      });
      
      // slow 对应的定时器仍会继续运行。

      空可迭代对象会产生以空数组兑现的 Promise⁠:

      Promise.all([]).then((values) => {
        console.log(values); // []
      });

      4.6.4 Promise.allSettled

      Promise.allSettled(iterable) 等待所有输入结束⁠,并返回每项结果的状态记录⁠:

      Promise.allSettled([
        Promise.resolve("A"),
        Promise.reject(
          new Error("B failed"),
        ),
        Promise.resolve("C"),
      ]).then((results) => {
        for (const result of results) {
          if (result.status === "fulfilled") {
            console.log(result.value);
          } else {
            console.error(result.reason);
          }
        }
      });

      结果形式为⁠:

      {
        status: "fulfilled",
        value: result,
      }

      或⁠:

      {
        status: "rejected",
        reason: error,
      }

      它适合批量任务彼此独立⁠,并且需要收集全部成功与失败结果的场景⁠。

      4.6.5 Promise.race

      Promise.race(iterable) 采用第一个敲定的输入结果⁠,无论该输入兑现还是拒绝⁠:

      Promise.race([
        delayValue("fast", 100),
        delayValue("slow", 1000),
      ]).then((result) => {
        console.log(result); // fast
      });

      空可迭代对象产生的 Promise 会一直保持 pending⁠:

      const pendingForever = Promise.race([]);

      Promise.race() 本身不会取消未获胜的操作⁠。

      4.6.6 Promise.any

      Promise.any(iterable) 采用第一个兑现的结果⁠:

      Promise.any([
        Promise.reject(
          new Error("source A failed"),
        ),
        delayValue("source B", 100),
        delayValue("source C", 200),
      ]).then((value) => {
        console.log(value); // source B
      });

      只有所有输入都拒绝时⁠,结果才拒绝⁠,并以 AggregateError 提供拒绝原因集合⁠:

      Promise.any([
        Promise.reject(
          new Error("A failed"),
        ),
        Promise.reject(
          new Error("B failed"),
        ),
      ]).catch((error) => {
        console.log(
          error instanceof AggregateError,
        ); // true
      
        console.log(error.errors);
      });

      空可迭代对象没有任何可能兑现的输入⁠,因此会以 AggregateError 拒绝⁠。

      4.6.7 并发方法对照

      方法兑现条件拒绝条件典型用途
      Promise.all()全部兑现任一拒绝所有结果都不可缺少
      Promise.allSettled()所有输入均已敲定通常不因输入拒绝而拒绝收集全部结果
      Promise.race()第一个敲定项为兑现第一个敲定项为拒绝竞争第一个结果
      Promise.any()任一输入兑现全部拒绝采用第一个成功结果

      聚合方法统一接收可迭代对象⁠,而不只接收数组⁠。

      4.6.8 Promise.withResolvers

      ECMAScript 2024 引入了 Promise.withResolvers()⁠。它返回 Promise 以及对应的决议函数⁠:

      const {
        promise,
        resolve,
        reject,
      } = Promise.withResolvers();
      
      setTimeout(() => {
        resolve("completed");
      }, 100);
      
      promise.then((value) => {
        console.log(value);
      });

      它近似于⁠:

      let resolve;
      let reject;
      
      const promise = new Promise((
        resolvePromise,
        rejectPromise,
      ) => {
        resolve = resolvePromise;
        reject = rejectPromise;
      });

      Promise.withResolvers() 适合把 Promise 决议能力交给事件源⁠、适配旧回调 API 或构建队列⁠。但将 resolve 和 reject 大范围暴露会增加状态管理难度⁠,应限制其作用域⁠。

      4.6.9 Promise.try

      ECMAScript 2025 引入了 Promise.try()⁠。它调用给定函数⁠,并把以下结果统一为 Promise⁠:

      • 普通返回值⁠;
      • 返回的 Promise 或 thenable⁠;
      • 同步抛出的异常⁠。
      function parseConfiguration(text) {
        return JSON.parse(text);
      }
      
      Promise.try(
        parseConfiguration,
        '{"theme":"dark"}',
      )
        .then((configuration) => {
          console.log(configuration);
        })
        .catch((error) => {
          console.error(error);
        });

      在目标运行环境尚未支持较新的静态方法时⁠,需要使用构建工具⁠、兼容层或传统写法⁠。

      4.7 async 函数

      4.7.1 async 函数总是返回 Promise

      async function getValue() {
        return 42;
      }
      
      const result = getValue();
      
      console.log(
        result instanceof Promise,
      ); // true

      普通返回值会成为返回 Promise 的兑现值⁠:

      getValue().then((value) => {
        console.log(value); // 42
      });

      函数内部未处理的异常会使返回 Promise 被拒绝⁠:

      async function fail() {
        throw new Error("failed");
      }
      
      fail().catch((error) => {
        console.error(error.message);
      });

      即使 async 函数返回现有 Promise⁠,调用结果也不要求与该 Promise 是同一个对象⁠:

      const original = Promise.resolve(42);
      
      async function getOriginal() {
        return original;
      }
      
      const wrapped = getOriginal();
      
      console.log(
        wrapped === original,
      ); // false

      它们会具有相同的最终结果⁠,但对象身份不同⁠。

      4.7.2 async 函数先同步执行

      调用 async 函数时⁠,函数体会立即同步执行⁠,直到⁠:

      • 遇到 await 并挂起⁠;
      • 执行 return⁠;
      • 抛出异常⁠;
      • 函数结束⁠。
      async function run() {
        console.log("async start");
      
        await Promise.resolve();
      
        console.log("async continuation");
      }
      
      console.log("script start");
      
      run();
      
      console.log("script end");

      输出为⁠:

      script start
      async start
      script end
      async continuation

      async 不会把整个函数自动推迟到未来执行⁠。

      4.8 await

      4.8.1 await 的基本语义

      await expression 会先通过 Promise 决议过程处理表达式的值⁠,然后挂起当前 async 函数或允许顶级 await 的模块求值⁠。

      Promise 兑现后⁠,await 表达式产生兑现值⁠:

      async function load() {
        const value = await Promise.resolve(42);
      
        console.log(value);
      }
      
      load();

      Promise 拒绝后⁠,await 在恢复位置产生抛出完成⁠:

      async function load() {
        try {
          await Promise.reject(
            new Error("failed"),
          );
        } catch (error) {
          console.error(error.message);
        }
      }

      4.8.2 await 不会阻塞整个线程

      await 只挂起当前异步函数的后续执行⁠。当前线程可以继续执行其他同步代码⁠、任务和微任务⁠:

      function delay(milliseconds) {
        return new Promise((resolve) => {
          setTimeout(resolve, milliseconds);
        });
      }
      
      async function run() {
        console.log("before await");
      
        await delay(1000);
      
        console.log("after await");
      }
      
      run();
      
      console.log("outside function");

      输出为⁠:

      before await
      outside function
      after await

      不应描述为“⁠await 释放一个被函数独占的主线程⁠”⁠。更准确的说法是⁠:当前异步函数的执行上下文被挂起⁠,其后续恢复由 Promise Job 调度⁠;事件循环可以继续推进其他工作⁠。

      4.8.3 await 普通值也会让出当前同步执行

      即使等待的不是 Promise⁠,后续代码也不会在当前同步段立即继续⁠:

      async function run() {
        console.log("A");
      
        await 42;
      
        console.log("C");
      }
      
      run();
      
      console.log("B");

      输出为⁠:

      A
      B
      C

      这是因为 await 会通过 Promise 决议过程处理值⁠,并把恢复步骤安排到后续 Job⁠。

      4.8.4 await 的使用位置

      await 可以出现在⁠:

      • async 函数体内⁠;
      • ECMAScript 模块顶层⁠。

      不能直接出现在普通函数或传统脚本顶层⁠:

      function invalid() {
        // SyntaxError
        // const value = await loadData();
      }

      顶级 await 会影响模块依赖图的求值过程⁠,将在第九章模块系统中讨论⁠。

      4.8.5 try…catch 与 await

      await 可以把 Promise 拒绝纳入普通异常控制流⁠:

      async function loadUser() {
        try {
          const user = await fetchUser();
          return user;
        } catch (error) {
          console.error(
            "Failed to load user:",
            error,
          );
      
          return null;
        }
      }

      只有实际等待或返回到当前处理范围内的 Promise⁠,错误才能被该 try...catch 捕获⁠。

      下面的函数没有等待异步操作⁠:

      async function incorrect() {
        try {
          startAsyncOperation();
        } catch (error) {
          // 无法捕获 startAsyncOperation()
          // 返回的 Promise 在未来发生的拒绝。
        }
      }

      应显式等待⁠:

      async function correct() {
        try {
          await startAsyncOperation();
        } catch (error) {
          console.error(error);
        }
      }

      更完整的异常分类与重新抛出原则将在第八章讨论⁠。

      4.9 串行与并发

      4.9.1 具有依赖关系的串行操作

      后一步依赖前一步结果时⁠,应顺序等待⁠:

      async function loadOrderSummary(userId) {
        const user = await fetchUser(userId);
        const orders = await fetchOrders(user.id);
        const summary = await createSummary(orders);
      
        return summary;
      }

      执行顺序明确为⁠:

      fetchUser
      → fetchOrders
      → createSummary

      4.9.2 独立操作不应无意串行化

      下面两个请求互不依赖⁠,却被依次启动⁠:

      async function loadPage() {
        const user = await fetchUser();
        const settings = await fetchSettings();
      
        return {
          user,
          settings,
        };
      }

      fetchSettings() 要等 fetchUser() 完成后才开始⁠。

      可以先启动两个操作⁠,再统一等待⁠:

      async function loadPage() {
        const userPromise = fetchUser();
        const settingsPromise = fetchSettings();
      
        const [
          user,
          settings,
        ] = await Promise.all([
          userPromise,
          settingsPromise,
        ]);
      
        return {
          user,
          settings,
        };
      }

      也可以直接写成⁠:

      async function loadPage() {
        const [
          user,
          settings,
        ] = await Promise.all([
          fetchUser(),
          fetchSettings(),
        ]);
      
        return {
          user,
          settings,
        };
      }

      这属于并发等待⁠,并不保证网络工作在不同 CPU 核心上执行⁠。

      4.9.3 Array.prototype.forEach 不会等待异步回调

      下面的代码不会等待所有保存操作结束⁠:

      async function saveAll(items) {
        items.forEach(async (item) => {
          await saveItem(item);
        });
      
        console.log("completed");
      }

      forEach() 忽略回调返回值⁠,因此外层函数会提前继续⁠。

      需要串行处理时⁠,使用 for...of⁠:

      async function saveAllSerially(items) {
        for (const item of items) {
          await saveItem(item);
        }
      
        console.log("completed");
      }

      需要并发处理时⁠,使用 map() 创建 Promise 数组⁠,再交给 Promise.all()⁠:

      async function saveAllConcurrently(items) {
        await Promise.all(
          items.map(item => saveItem(item)),
        );
      
        console.log("completed");
      }

      4.9.4 并发数量也需要控制

      一次启动过多请求可能导致⁠:

      • 浏览器或服务器连接压力⁠;
      • API 限流⁠;
      • 内存占用增加⁠;
      • 数据库或文件系统负载过高⁠;
      • 单次失败影响大量工作⁠。

      下面的辅助函数以固定数量的工作循环处理任务⁠:

      async function mapWithConcurrency(
        values,
        concurrency,
        mapper,
      ) {
        if (
          !Number.isInteger(concurrency)
          || concurrency <= 0
        ) {
          throw new RangeError(
            "concurrency 必须是正整数",
          );
        }
      
        const results = new Array(
          values.length,
        );
      
        let nextIndex = 0;
      
        async function worker() {
          while (true) {
            const currentIndex = nextIndex;
      
            if (currentIndex >= values.length) {
              return;
            }
      
            nextIndex += 1;
      
            results[currentIndex] = await mapper(
              values[currentIndex],
              currentIndex,
            );
          }
        }
      
        const workerCount = Math.min(
          concurrency,
          values.length,
        );
      
        const workers = Array.from(
          {
            length: workerCount,
          },
          () => worker(),
        );
      
        await Promise.all(workers);
      
        return results;
      }

      使用示例⁠:

      async function loadAll(urls) {
        return mapWithConcurrency(
          urls,
          4,
          async (url) => {
            const response = await fetch(url);
      
            if (!response.ok) {
              throw new Error(
                `HTTP ${response.status}`,
              );
            }
      
            return response.json();
          },
        );
      }

      这种实现会在任一工作循环拒绝时使最终 Promise 拒绝⁠,但已经启动的其他操作不会自动取消⁠。生产代码还应根据需求设计失败策略和取消机制⁠。

      4.10 Promise 的取消与 AbortController

      Promise 本身没有通用的取消方法⁠。取消的是底层操作⁠,而不是 Promise 这个结果对象⁠。

      DOM 标准提供 AbortController 和 AbortSignal⁠,供支持取消的 API 接收统一信号⁠。

      4.10.1 取消 fetch 请求

      async function requestData() {
        const controller = new AbortController();
      
        const requestPromise = fetch(
          "/api/data",
          {
            signal: controller.signal,
          },
        );
      
        controller.abort(
          new Error("Request cancelled"),
        );
      
        try {
          const response = await requestPromise;
          console.log(response);
        } catch (error) {
          console.error(error);
        }
      }
      
      requestData();

      调用 abort() 会把信号标记为已中止⁠。支持该信号的 API 应停止相应活动⁠,并使尚未敲定的 Promise 以中止原因拒绝⁠。

      是否能立即停止底层网络传输⁠,仍取决于 API 和宿主实现⁠。可靠的编程接口是⁠:后续逻辑可以通过拒绝状态得知操作已被中止⁠。

      4.10.2 请求超时

      可以通过定时器和 AbortController 实现超时控制⁠:

      async function fetchWithTimeout(
        input,
        options = {},
        timeout = 5000,
      ) {
        const controller = new AbortController();
      
        const timerId = setTimeout(() => {
          controller.abort(
            new Error(
              `Request timed out after ${timeout} ms`,
            ),
          );
        }, timeout);
      
        try {
          const response = await fetch(
            input,
            {
              ...options,
              signal: controller.signal,
            },
          );
      
          return response;
        } finally {
          clearTimeout(timerId);
        }
      }

      如果调用者已经提供自己的 signal⁠,实际工程还需要组合外部取消信号与超时信号⁠。现代环境可以研究 AbortSignal.timeout() 和 AbortSignal.any()⁠,但使用前应确认目标运行环境支持情况⁠。

      4.10.3 fetch 与 HTTP 错误状态

      fetch() 在收到 HTTP 404⁠、500 等响应时⁠,通常仍然以 Response 对象兑现⁠。它主要在网络错误⁠、CORS 失败或中止等情况下拒绝⁠。

      因此⁠,需要显式检查 HTTP 状态⁠:

      async function fetchJson(url, options) {
        const response = await fetch(
          url,
          options,
        );
      
        if (!response.ok) {
          throw new Error(
            `HTTP ${response.status}: ${response.statusText}`,
          );
        }
      
        return response.json();
      }

      Promise 的拒绝只表达 API 规定的失败通道⁠,不能代替业务层和协议层状态检查⁠。

      4.11 未处理的 Promise

      4.11.1 不应忽略返回的 Promise

      下面的调用启动了异步操作⁠,但没有等待或注册拒绝处理器⁠:

      saveData();

      如果 saveData() 返回的 Promise 拒绝⁠,宿主环境可能报告未处理的 Promise 拒绝⁠。

      应选择一种明确方式⁠:

      await saveData();

      或⁠:

      saveData().catch((error) => {
        console.error(error);
      });

      如果确实有意启动不需要等待的任务⁠,可以用 void 明确表达⁠,并在内部或外部处理错误⁠:

      void saveData().catch((error) => {
        reportError(error);
      });

      void 不会处理错误⁠,它只明确表示调用方有意忽略 Promise 的兑现值⁠。

      4.11.2 catch 不应无条件吞掉错误

      promise.catch(() => {});

      这种写法会把错误转换为正常兑现的 undefined⁠,并隐藏失败原因⁠。

      只有在明确允许降级时才应返回后备值⁠:

      async function loadPageData() {
        const data = await loadOptionalData()
          .catch((error) => {
            logWarning(error);
            return [];
          });
      
        return data;
      }

      未知或无法处理的错误应继续向上传递⁠。详细原则见第八章⁠。

      4.12 综合示例⁠:异步数据流水线

      下面的示例包含三个具有依赖关系的阶段⁠:

      1. 读取原始数据⁠;
      2. 验证并转换数据⁠;
      3. 保存处理结果⁠。
      function delay(milliseconds) {
        return new Promise((resolve) => {
          setTimeout(resolve, milliseconds);
        });
      }
      
      async function fetchRawData() {
        await delay(50);
      
        return {
          id: 1024,
          payload: "data_stream",
        };
      }
      
      async function transformData(rawData) {
        await delay(50);
      
        if (
          rawData === null
          || typeof rawData.payload !== "string"
        ) {
          throw new TypeError(
            "rawData.payload 必须是字符串",
          );
        }
      
        return {
          id: rawData.id,
          payload: rawData.payload.toUpperCase(),
        };
      }
      
      async function saveData(processedData) {
        await delay(50);
      
        return {
          status: "saved",
          data: processedData,
        };
      }
      
      async function runPipeline() {
        const startTime = performance.now();
      
        try {
          const rawData = await fetchRawData();
      
          const processedData = await transformData(
            rawData,
          );
      
          const result = await saveData(
            processedData,
          );
      
          return result;
        } catch (error) {
          console.error(
            "Pipeline failed:",
            error,
          );
      
          throw error;
        } finally {
          const elapsed = (
            performance.now() - startTime
          );
      
          console.log(
            `Elapsed: ${elapsed.toFixed(2)} ms`,
          );
        }
      }
      
      runPipeline()
        .then((result) => {
          console.log(result);
        })
        .catch((error) => {
          console.error(
            "Unhandled pipeline failure:",
            error,
          );
        });

      这个示例中的三个阶段存在数据依赖⁠,因此顺序 await 是合理的⁠。finally 只负责记录耗时⁠,不覆盖原有成功或失败结果⁠。

      4.13 异步控制流的选择

      方式主要特点适用场景主要风险
      回调函数API 决定调用时机事件监听⁠、旧式接口⁠、简单通知控制反转⁠、嵌套与错误处理分散
      Promise 链返回新 Promise⁠,支持结果传递和错误传播数据转换流水线⁠、函数式组合忘记返回内部 Promise⁠、链路过长
      async/await使用普通控制流组织 Promise多步骤业务逻辑⁠、条件和循环无意串行化⁠、忽略返回 Promise
      Promise 聚合方法组合多个并发结果批量请求⁠、竞争和容错不自动取消剩余操作⁠、并发量过高

      Promise 链与 async/await 并不是两套不同的底层异步系统⁠。async/await 建立在 Promise 语义上⁠,可以与 .then()⁠、.catch() 和聚合方法混合使用⁠。

      选择方式时应优先考虑⁠:

      • 数据依赖关系⁠;
      • 错误传播范围⁠;
      • 是否需要并发⁠;
      • 是否需要取消⁠;
      • 是否需要限制并发数量⁠;
      • 代码是否便于测试和维护⁠。

      4.14 本章小结

      本章建立了浏览器环境下的 JavaScript 异步执行模型⁠:

      1. 异步编程允许程序在等待外部结果时推进其他工作⁠,但不会自动把 CPU 密集型代码移出主线程⁠;
      2. 浏览器可以包含多个线程和多个 JavaScript Agent⁠,同一 Agent 中的普通回调仍按事件循环调度⁠;
      3. HTML 标准使用任务和任务队列描述宿主调度⁠,“⁠宏任务⁠”只是常见的非正式术语⁠;
      4. 浏览器并非只有一个简单的全局任务队列⁠,不同任务来源之间的选择由宿主调度⁠;
      5. 一次任务结束后会执行适当的微任务检查点⁠,并持续处理到微任务队列为空⁠;
      6. 渲染只在存在渲染机会且确有必要时进行⁠,并非每次任务后都必定绘制⁠;
      7. 回调可以同步或异步⁠,错误优先回调主要是 Node.js 传统约定⁠;
      8. Promise 具有 pending⁠、fulfilled 和 rejected 三种状态⁠,settled 与 resolved 的含义并不完全相同⁠;
      9. Promise 执行器同步运行⁠,Promise 反应回调通过后续 Job 异步执行⁠;
      10. .then() 每次返回新的 Promise⁠,处理器的返回值或异常决定新 Promise 的结果⁠;
      11. .catch() 可以集中处理上游拒绝⁠,.finally() 默认保持原结果⁠,但自身失败会覆盖原结果⁠;
      12. Promise.all()⁠、allSettled()⁠、race() 和 any() 对应不同的并发组合语义⁠;
      13. async 函数总是返回 Promise⁠,并同步执行到第一个挂起点⁠;
      14. await 只挂起当前异步函数或模块求值⁠,不会阻塞整个事件循环⁠;
      15. 独立异步操作应并发启动⁠,存在数据依赖的操作才应顺序等待⁠;
      16. forEach() 不会等待异步回调⁠,串行处理应使用 for...of⁠,并发处理可使用 Promise.all()⁠;
      17. Promise 本身没有通用取消能力⁠,支持取消的宿主 API 通常使用 AbortController 和 AbortSignal⁠;
      18. fetch() 收到 HTTP 错误状态时通常仍会兑现⁠,需要显式检查 response.ok⁠;
      19. 所有启动的 Promise 都应具有明确的等待⁠、返回或拒绝处理路径⁠。

      参考资料

      1. javascript.info: Event loop—microtasks and macrotasks
      2. javascript.info: Introduction—callbacks
      3. javascript.info: Promise
      4. javascript.info: Promises chaining
      5. javascript.info: Error handling with promises
      6. javascript.info: Promise API
      7. javascript.info: Microtasks
      8. javascript.info: Async/await
      9. javascript.info: Fetch—Abort
      10. MDN JavaScript Guide: Using promises
      11. ECMAScript 2026 Language Specification: Promise Objects
      12. ECMAScript 2026 Language Specification: Async Function Definitions
      13. ECMAScript 2026 Language Specification: Await
      14. HTML Standard: Event Loops
      15. HTML Standard: Timers
      16. DOM Standard: Aborting Ongoing Activities
      17. Fetch Standard
      上一篇 JavaScript 3. 数据结构与算法基础 2026 年 7 月 19 日 下一篇 JavaScript 5. 对象、集合与数组 2026 年 7 月 19 日
      © 2026 CHEN Hua All rights reserved
      闽ICP备2026003335号 · 粤公网安备44030002014022号
      © Hua Chen / PhysChen.com