JavaScript 的单线程特性决定了其异步编程模型的核心——事件循环(Event Loop)。理解事件循环、宏任务(MacroTask)和微任务(MicroTask)的机制,是掌握前端异步编程的关键。本文将从基础概念到进阶场景,结合案例详细解析这一机制。
一、事件循环基础
1. 为什么需要事件循环?
JavaScript 是单线程语言,同步代码会阻塞主线程,导致页面无响应。例如:
console.log("Start");
while (true) {} // 死循环,页面卡死
console.log("End"); // 永远不会执行
事件循环通过将异步任务放入任务队列,避免主线程阻塞,实现“非阻塞”效果。
2. 事件循环的核心机制
- 执行栈(Call Stack):同步代码按顺序执行,形成调用栈。
- 任务队列(Task Queue):异步任务完成后,回调函数进入任务队列。
- 事件循环:主线程执行栈清空后,从任务队列中取出任务执行。
二、宏任务与微任务
1. 宏任务(MacroTask)
- 定义:由宿主环境(浏览器/Node.js)提供的异步任务,每次执行一个宏任务。
- 常见宏任务:
setTimeout
/setInterval
I/O
操作(如文件读写)UI 渲染
事件监听
(如click
)setImmediate
(Node.js 特有)script
(整体代码)
2. 微任务(MicroTask)
- 定义:在当前宏任务执行完毕后、下一个宏任务执行前执行的异步任务。
- 常见微任务:
Promise.then
/catch
/finally
MutationObserver
(监听 DOM 变化)queueMicrotask
(现代浏览器原生 API)process.nextTick
(Node.js 特有,优先级最高)
3. 执行顺序规则
- 执行同步代码(宏任务)。
- 执行所有微任务。
- 渲染页面(浏览器环境)。
- 执行下一个宏任务。
- 重复上述步骤。
三、案例解析
案例 1:基础执行顺序
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
}).then(() => {
console.log("Promise 2");
});
console.log("End");
输出:
Start
End
Promise 1
Promise 2
setTimeout
解析:
- 同步代码
Start
和End
执行。 - 微任务队列:
Promise 1
→Promise 2
,依次执行。 - 宏任务队列:
setTimeout
执行。
案例 2:微任务嵌套
console.log("Start");
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve().then(() => {
console.log("Promise in setTimeout");
});
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
}).then(() => {
console.log("Promise 2");
});
console.log("End");
输出:
Start
End
Promise 1
Promise 2
setTimeout 1
Promise in setTimeout
setTimeout 2
解析:
- 同步代码
Start
和End
执行。 - 微任务队列:
Promise 1
→Promise 2
执行。Promise 1
的setTimeout 2
进入宏任务队列。
- 宏任务队列:
setTimeout 1
执行,内部Promise in setTimeout
进入微任务队列。setTimeout 2
执行。
案例 3:process.nextTick
(Node.js 环境)
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
process.nextTick(() => {
console.log("nextTick 1");
process.nextTick(() => {
console.log("nextTick 2");
});
});
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
输出(Node.js):
Start
End
nextTick 1
nextTick 2
Promise
setTimeout
解析:
process.nextTick
的优先级高于微任务,在当前宏任务结束后立即执行。- 嵌套的
nextTick 2
会在当前nextTick
阶段完成后执行。
四、进阶详解
1. 浏览器与 Node.js 的差异
- 浏览器:
- 微任务:
Promise.then
、MutationObserver
。 - 宏任务:
setTimeout
、I/O
、UI 渲染
。
- 微任务:
- Node.js:
- 微任务:
Promise.then
、queueMicrotask
、process.nextTick
。 - 宏任务:
setTimeout
、setImmediate
、I/O
。 process.nextTick
优先级高于其他微任务。
- 微任务:
2. 避免常见问题
- 死循环:同步代码中的死循环会阻塞事件循环。
- 微任务滥用:在微任务中执行大量操作会导致页面渲染延迟。
setImmediate
vssetTimeout(fn, 0)
:setImmediate
:在当前事件循环的下一个阶段执行。setTimeout(fn, 0)
:在下一个事件循环的宏任务阶段执行。
3. 优化建议
- 使用
async/await
:简化异步代码,避免回调地狱。 - 合理使用微任务:将高优先级的任务(如 DOM 更新)放入微任务。
- 避免同步阻塞:将耗时操作(如复杂计算)放入 Web Worker。
五、总结
- 事件循环是 JavaScript 异步编程的核心,通过宏任务和微任务实现非阻塞。
- 执行顺序:同步代码 → 微任务 → 渲染 → 宏任务。
- 微任务优先级高于宏任务,
process.nextTick
(Node.js)优先级最高。 - 合理使用异步机制,避免阻塞主线程,提升页面性能。