异步编程与事件循环
JavaScript 在单线程上通过事件循环调度异步任务。前置:语言基础。
一、事件循环(Event Loop)
JavaScript 是单线程的,事件循环是其实现异步非阻塞 I/O 的核心机制。
1. 宏任务(MacroTask)与微任务(MicroTask)
浏览器(HTML 规范中的 task)常见划分:
- 宏任务(task):
setTimeout/setInterval、MessageChannel、I/O 回调、用户事件等。 - 微任务(microtask):
Promise回调、queueMicrotask()、MutationObserver等。
Node.js 另有 process.nextTick(在当前阶段之后、微任务之前执行)与 setImmediate(check 阶段),与浏览器队列细节不同,勿混为一谈。
requestAnimationFrame 在浏览器中用于下一帧绘制前回调,不属于上述微任务队列,通常在本轮微任务清空后、渲染前执行。
2. 执行顺序
- 执行一个宏任务(从宏任务队列中取出)
- 执行当前宏任务关联的所有微任务(清空微任务队列)
- 必要时进行 UI 渲染(浏览器)
- 进入下一轮循环,执行下一个宏任务
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 输出: 1, 4, 3, 2
3. 常见误区
setTimeout(fn, 0)并不立即执行,它需要等待当前宏任务和微任务全部完成。- 微任务中可以继续添加微任务,会一直执行直到微任务队列为空(可能导致“卡死”)。
二、回调地狱与解救
1. 回调地狱(Callback Hell)
多个异步操作层层嵌套,导致代码难以阅读和维护。
getData(function (a) {
getMoreData(a, function (b) {
getMoreMoreData(b, function (c) {
console.log(c);
});
});
});
2. Promise 链式调用
Promise 是一个代表异步操作最终完成(或失败)的对象。通过 .then() 和 .catch() 实现链式调用,避免嵌套。
getData()
.then(a => getMoreData(a))
.then(b => getMoreMoreData(b))
.then(c => console.log(c))
.catch(err => console.error(err));
3. Promise 静态方法
Promise.all(iterable):所有成功才成功,任一失败立即失败(返回失败原因)。Promise.allSettled(iterable):等待所有完成(无论成功或失败),返回每个结果的状态和值。Promise.any(iterable):任意一个成功即成功,全部失败才失败(返回AggregateError)。Promise.race(iterable):返回最先敲定(成功或失败)的结果。
4. 错误冒泡
在 Promise 链中,catch 可以捕获链条中任意位置抛出的错误(同步或异步)。若没有 catch,错误会被静默吞没(现代浏览器/Node.js 会触发 unhandledrejection 事件)。
三、async/await
async/await 是 Promise 的语法糖,使异步代码看起来像同步代码,更易于阅读和调试。
1. 原理:生成器(Generator)+ 自动执行器
async 函数内部会被编译成一个生成器函数(Generator),配合类似 co 库的自动执行器,每次 await 对应一次 yield,自动处理 next() 调用和 Promise 解析。
2. 基本用法
async function fetchData() {
try {
const a = await getData();
const b = await getMoreData(a);
return b;
} catch (err) {
console.error(err);
}
}
3. 错误处理
try/catch:可以捕获await抛出的 rejected Promise,也捕获同步错误。.catch():在await后直接链式调用.catch()。
const data = await fetch(url)
.then(res => res.json())
.catch(err => null);
注意:async 函数总是返回一个 Promise,如果函数内没有 return,则返回 Promise.resolve(undefined)。
4. 顶层 await(ES2022)
在模块中可以直接使用 await 而不需要包裹 async 函数,适用于模块初始化依赖异步操作。
四、异步迭代器(for await...of)与异步生成器
1. 异步迭代器(Async Iterator)
一个对象实现了 Symbol.asyncIterator 方法,该方法返回一个对象,其 next() 方法返回 Promise<{value, done}>。可以使用 for await...of 循环消费。
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
next() {
if (i < 3) {
return Promise.resolve({ value: i++, done: false });
}
return Promise.resolve({ done: true });
},
};
},
};
(async () => {
for await (const num of asyncIterable) {
console.log(num); // 0,1,2
}
})();
2. 异步生成器(Async Generator)
使用 async function* 定义,可以 yield Promise 或直接 yield 值,for await...of 会自动等待每个 yield 的 Promise 解析。
async function* generateSequence() {
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 100)); // 模拟异步
yield i;
}
}
(async () => {
for await (const val of generateSequence()) {
console.log(val); // 0,1,2 (每个间隔 100ms)
}
})();
3. 典型应用
- 处理分页 API 的流式数据。
- 读取大文件(Node.js 流)。
- 生成无限序列(如 WebSocket 消息流)。
五、AbortController 中止异步操作
AbortController 提供了一个标准方式来取消一个或多个异步操作(例如 fetch 请求、事件监听等)。核心思想:通过信号(signal)传递,调用 abort() 方法触发中断。
1. 基本用法
const controller = new AbortController();
const { signal } = controller;
fetch("/api/data", { signal })
.then(res => res.json())
.catch(err => {
if (err.name === "AbortError") {
console.log("Fetch aborted");
} else {
console.error("Other error:", err);
}
});
// 取消请求
controller.abort();
2. 中止多个请求
同一个 signal 可以传入多个 fetch,调用一次 abort() 会取消所有相关请求。
3. 中止其他异步操作(如事件监听)
任何支持 signal 的 API 都可以使用,例如 addEventListener 的 AbortSignal:
const controller = new AbortController();
element.addEventListener("click", handler, { signal: controller.signal });
// 后续调用 controller.abort() 会移除该监听器
4. 手动实现可中止的异步任务
可以监听 signal 的 abort 事件,自定义清理逻辑。
function wait(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
});
});
}
const controller = new AbortController();
wait(5000, controller.signal).catch(err => console.log(err.name));
controller.abort(); // 立即中止
5. 注意要点
AbortController只能触发一次abort,不可复用。- 浏览器原生支持
fetch和addEventListener,Node.js 从 v15+ 开始支持AbortController用于fs、http等模块。
总结
| 概念 | 核心要点 |
|---|---|
| 事件循环 | 宏任务与微任务的优先级模型,决定了异步代码的执行时机 |
| Promise | 链式调用、静态方法(all/allSettled/any/race)、错误冒泡 |
| async/await | 基于生成器 + 自动执行器的语法糖,配合 try/catch 或 .catch 处理错误 |
| 异步迭代器/生成器 | for await...of 消费异步数据流,async function* 生产异步序列 |
| AbortController | 通过 signal 取消 fetch、事件监听等,可自定义中止逻辑 |
掌握这些内容,就能高效处理 JavaScript 中几乎所有的异步场景,写出清晰、健壮且可取消的异步代码。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| MDN:并发模型与事件循环 | 事件循环 |
| MDN:Promise | Promise API |
| MDN:async function | async/await |
| MDN:AbortController | 中止异步操作 |
| HTML:event loop | 浏览器事件循环(英文) |
相关文章
错误处理与调试
结构化错误类型、try/catch 与全局未捕获处理是健壮应用的基础。
浏览器相关 API
DOM 事件、存储、fetch、Worker、Observer 等由浏览器提供的 Web API(非 ECMAScript 核心)。HTML 结构见 标签。
性能优化
防抖/节流、渲染与内存、资源预加载及引擎行为是前端性能的主要抓手。相关 CSS 见 视觉效果、Head 资源提示。
模块化与工程化
ESM、CommonJS 与打包工具(Tree Shaking、代码分割)是前端工程化的基础。入门见 JavaScript 基础 · 模块。
执行模型与作用域
执行上下文与词法作用域决定变量在何处可访问、闭包如何形成,以及 var / let / const 的差异。语言概览见 JavaScript 基础。
数据结构与集合增强
Map / Set、WeakMap / WeakSet、数组高阶方法、TypedArray 与 Symbol 等扩展了内置数据结构能力。见 语言基础。