目录
JavaScript 4. 异步编程与事件循环
JavaScript 程序经常需要等待网络响应、定时器、用户交互、文件读写或其他外部事件。如果程序在等待期间持续占用执行线程,页面将无法及时响应输入,服务器也难以同时处理其他工作。
异步编程的目的不是让所有代码自动并行执行,而是让程序能够在某项工作尚未完成时,把执行机会交给其他任务,并在结果可用后继续处理。
理解 JavaScript 异步编程,需要区分三个层次:
- ECMAScript 语言层定义函数、Promise、
async、await和 Job 等语义; - 宿主环境提供定时器、网络请求、用户事件和事件循环;
- 运行时实现决定线程、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
原因是:
- 当前脚本作为任务开始执行;
- 定时器登记后等待未来任务调度;
- Promise 回调和
queueMicrotask()回调进入微任务队列; - 当前同步脚本结束;
- 浏览器执行微任务检查点,依次清空微任务队列;
- 定时器满足条件后,其回调才有机会作为后续任务执行。
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 渲染机会
常见教学模型会写成:
执行一个任务
→ 清空微任务
→ 页面渲染
→ 执行下一个任务
这个模型有助于理解基本顺序,但不能理解为每个任务结束后浏览器都必定绘制一帧。
浏览器只会在存在渲染机会且确有更新需要时执行相关工作。刷新率、页面可见性、设备性能和浏览器调度策略都会影响实际渲染时机。
因此,只能稳定地认为:
- 一个任务执行期间不会被另一个普通任务任意插入;
- 任务结束后会进行适当的微任务检查点;
- 浏览器可能在合适的渲染机会更新页面;
- 之后事件循环继续选择可运行任务。
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 综合示例:异步数据流水线
下面的示例包含三个具有依赖关系的阶段:
- 读取原始数据;
- 验证并转换数据;
- 保存处理结果。
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 异步执行模型:
- 异步编程允许程序在等待外部结果时推进其他工作,但不会自动把 CPU 密集型代码移出主线程;
- 浏览器可以包含多个线程和多个 JavaScript Agent,同一 Agent 中的普通回调仍按事件循环调度;
- HTML 标准使用任务和任务队列描述宿主调度,“宏任务”只是常见的非正式术语;
- 浏览器并非只有一个简单的全局任务队列,不同任务来源之间的选择由宿主调度;
- 一次任务结束后会执行适当的微任务检查点,并持续处理到微任务队列为空;
- 渲染只在存在渲染机会且确有必要时进行,并非每次任务后都必定绘制;
- 回调可以同步或异步,错误优先回调主要是 Node.js 传统约定;
- Promise 具有 pending、fulfilled 和 rejected 三种状态,settled 与 resolved 的含义并不完全相同;
- Promise 执行器同步运行,Promise 反应回调通过后续 Job 异步执行;
.then()每次返回新的 Promise,处理器的返回值或异常决定新 Promise 的结果;.catch()可以集中处理上游拒绝,.finally()默认保持原结果,但自身失败会覆盖原结果;Promise.all()、allSettled()、race()和any()对应不同的并发组合语义;async函数总是返回 Promise,并同步执行到第一个挂起点;await只挂起当前异步函数或模块求值,不会阻塞整个事件循环;- 独立异步操作应并发启动,存在数据依赖的操作才应顺序等待;
forEach()不会等待异步回调,串行处理应使用for...of,并发处理可使用Promise.all();- Promise 本身没有通用取消能力,支持取消的宿主 API 通常使用
AbortController和AbortSignal; fetch()收到 HTTP 错误状态时通常仍会兑现,需要显式检查response.ok;- 所有启动的 Promise 都应具有明确的等待、返回或拒绝处理路径。
参考资料
- javascript.info: Event loop—microtasks and macrotasks
- javascript.info: Introduction—callbacks
- javascript.info: Promise
- javascript.info: Promises chaining
- javascript.info: Error handling with promises
- javascript.info: Promise API
- javascript.info: Microtasks
- javascript.info: Async/await
- javascript.info: Fetch—Abort
- MDN JavaScript Guide: Using promises
- ECMAScript 2026 Language Specification: Promise Objects
- ECMAScript 2026 Language Specification: Async Function Definitions
- ECMAScript 2026 Language Specification: Await
- HTML Standard: Event Loops
- HTML Standard: Timers
- DOM Standard: Aborting Ongoing Activities
- Fetch Standard