下篇:JavaScript 异步编程深度剖析 —— 事件循环、Promise、async/await 与并发模型
目录
3.任务队列(Task Queue / Callback Queue)
前言
JavaScript 是单线程的,却能高效处理 I/O 密集型任务,这得益于事件循环(Event Loop)。本篇将详细解析宏任务、微任务、Promise 内部原理以及 async/await 的语法糖本质。
一、为什么需要异步? —— 单线程的困境
假设 JavaScript 是纯粹同步的:一个网络请求需要等待 3 秒,那这 3 秒内整个页面将无法响应任何操作(UI 线程被阻塞)。为了避免这种情况,宿主环境(浏览器/Node.js)提供了异步 API(如定时器、Ajax、事件监听),并设计了一套事件循环机制来调度这些回调。
二、事件循环(Event Loop)的核心组件
事件循环本质上是一个永不停歇的循环,它的职责是监控调用栈(Call Stack)和任务队列(Task Queue),当调用栈为空时,从任务队列中取出一个任务执行。
1.调用栈(Call Stack)
-
存储函数执行上下文的栈结构。
-
每调用一个函数,就推入一个帧;函数返回时弹出。
-
栈空表示当前没有正在执行的同步代码。
2.堆(Heap)
-
用于存储对象、闭包等引用类型数据。
-
与事件循环没有直接关系,但垃圾回收会影响性能。
3.任务队列(Task Queue / Callback Queue)
-
存放宏任务(MacroTask) 的回调函数。
-
常见的宏任务:
setTimeout、setInterval、I/O、UI 渲染、setImmediate(Node.js)。
4.微任务队列(Microtask Queue)
-
存放微任务(MicroTask) 的回调函数,优先级高于宏任务。
-
常见的微任务:
Promise.then/catch/finally、MutationObserver、queueMicrotask。
执行顺序规则:
-
执行一个宏任务(从任务队列中取出)。
-
执行当前宏任务产生的所有微任务(清空微任务队列)。
-
可能进行 UI 渲染(浏览器)。
-
回到第 1 步,取出下一个宏任务。
关键点:微任务会在下一个宏任务之前全部执行完毕,因此微任务中递归添加微任务会阻塞宏任务的执行(但不会完全死循环,因为浏览器会限制递归深度)。
三、宏任务 vs 微任务 —— 经典面试题解析
1.基础顺序
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出顺序: 1, 4, 3, 2
解析:
-
同步代码:输出 1 和 4。
-
setTimeout回调进入宏任务队列。 -
Promise.then回调进入微任务队列。 -
同步代码执行完毕,调用栈空。
-
事件循环检查微任务队列,输出 3。
-
微任务队列清空后,取出宏任务队列中的
setTimeout回调,输出 2。
2.嵌套微任务
Promise.resolve()
.then(() => {
console.log('A');
Promise.resolve().then(() => console.log('B'));
})
.then(() => console.log('C'));
// 输出: A, B, C
为什么不是 A, C, B?
-
第一个
then回调执行时,输出 A,并向微任务队列添加了输出 B 的微任务。 -
第一个
then返回一个新的 Promise,它的then回调(输出 C)会被添加到微任务队列,但排在 B 之后。 -
当前宏任务结束后,清空微任务队列的顺序是:先 B,后 C。
四、Promise 的深入理解
Promise 是 ES6 引入的异步编程解决方案,它本质是一个状态机,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦改变,就不可逆。
1.构造函数与执行器
const promise = new Promise((resolve, reject) => {
// 异步操作
if (success) resolve(value);
else reject(error);
});
执行器函数会立即同步执行,而 then 和 catch 中的回调会作为微任务异步执行。
2. Promise 链式调用
then 方法返回一个新的 Promise,因此可以实现链式调用。链中的值会通过 return 传递。
Promise.resolve(1)
.then(x => x + 1)
.then(x => { throw new Error('oops'); })
.catch(err => console.log(err.message));
3.静态方法
-
Promise.all:所有 Promise 成功则返回结果数组,有一个失败则立即失败。 -
Promise.allSettled:等待所有 Promise 完成(无论成功或失败),返回状态数组。 -
Promise.race:返回最先完成(成功或失败)的 Promise 结果。 -
Promise.any:返回第一个成功的 Promise,若全部失败则抛出 AggregateError。
五、async/await —— 更优雅的异步语法
async 函数返回一个 Promise,await 表达式会暂停当前 async 函数的执行,等待右侧 Promise 解决后恢复执行,并返回解决的值。
本质:await 后面的代码相当于被包装在 Promise.then 中,因此属于微任务。
async function fetchData() {
console.log('start');
const data = await new Promise(resolve => setTimeout(() => resolve('data'), 1000));
console.log(data);
return 'done';
}
fetchData().then(console.log);
// 输出: start, (1秒后) data, done
错误处理:使用 try...catch 包裹 await,或者不捕获则返回 rejected Promise。
六、Node.js 中的事件循环差异
Node.js 的事件循环分为多个阶段:timers、pending callbacks、idle/prepare、poll、check、close。每个阶段都有一个对应的宏任务队列。
-
timers:执行
setTimeout和setInterval回调。 -
poll:获取新的 I/O 事件,执行 I/O 回调。
-
check:执行
setImmediate回调。
process.nextTick 既不是宏任务也不是微任务,它会在当前阶段结束后、下一阶段开始前执行,优先级高于微任务。
七、实战建议与常见陷阱
-
避免长时间阻塞调用栈:大量计算可以拆分成多个宏任务(使用
setTimeout分片)或使用 Web Worker。 -
合理使用微任务:微任务能更快执行,但过多微任务会延迟 UI 渲染。
-
Promise 必须处理错误:总是添加
.catch或在async函数中使用try/catch。 -
循环中的异步:
for循环中使用await会顺序执行;并发使用Promise.all。
// 顺序执行(慢)
for (const url of urls) {
await fetch(url);
}
// 并发执行(快)
await Promise.all(urls.map(url => fetch(url)));
下篇总结
事件循环是 JavaScript 非阻塞 I/O 模型的灵魂。理解宏任务、微任务的执行顺序,能帮你轻松应对异步代码的调试和面试题。结合 Promise 和 async/await,你可以写出既清晰又高效的异步代码。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)