JavaScript 事件循环:宏任务与微任务
JavaScript 是单线程语言,这意味着它一次只能做一件事。为了不阻塞主线程(导致页面卡顿),JavaScript 采用了一种“事件循环”机制来处理异步操作。理解这一机制的关键,在于分清“宏任务”和“微任务”的区别与执行顺序。
以下指南将直接解析核心概念,并通过代码演练帮你彻底掌握这一运行机制。
一、 核心概念:任务队列
浏览器或 Node.js 在执行代码时,会将任务分类存放在不同的队列中。你需要先搞清楚这两个队列里分别装什么。
| 任务类型 | 包含内容 | 常见 API |
|---|---|---|
| 宏任务 | 耗时较长的操作,脚本级别的代码 | script (整体代码), setTimeout, setInterval, setImmediate (Node.js), I/O 操作 |
| 微任务 | 耗时较短,需要高优先级执行的操作 | Promise.then/catch/finally, process.nextTick (Node.js), MutationObserver |
二、 执行流程:事件循环的运作逻辑
事件循环是一个无限循环的过程,它决定了代码执行的先后顺序。请按照以下逻辑步骤理解引擎的工作方式:
-
执行同步代码
引擎首先从头到尾执行当前的“主脚本”(这也是一个宏任务)。所有同步代码直接运行,遇到异步任务时,将其交给相应的 Web API 处理,并将回调函数注册到对应的任务队列中。 -
检查微任务队列
同步代码执行完毕后,引擎立即检查微任务队列。如果队列中有任务,一次性执行队列中的所有微任务,直到队列被清空。在此期间,如果有新的微任务产生,也会立即加入当前队列并继续执行。 -
执行 UI 渲染
当微任务队列完全清空后,浏览器通常会进行页面渲染(如果需要重绘或回流)。 -
执行一个宏任务
渲染完成后,引擎从宏任务队列中取出一个任务放入执行栈执行。 -
循环回到步骤 2
宏任务执行完毕后,再次检查微任务队列。这个“宏任务 -> 微任务 -> 渲染 -> 宏任务”的过程不断循环。
为了更直观地展示这一流程,请参考下方的逻辑图:
三、 实战演练:代码执行顺序推演
通过具体的代码案例,我们将上述理论转化为可预测的输出结果。
案例一:基础顺序测试
请看以下代码:
console.log('1. 开始');
setTimeout(() => {
console.log('2. setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise.then');
});
console.log('4. 结束');
推演步骤:
- 执行同步代码:
- 遇到
console.log('1. 开始'),输出1. 开始。 - 遇到
setTimeout,将其回调放入宏任务队列。 - 遇到
Promise.resolve().then,将其回调放入微任务队列。 - 遇到
console.log('4. 结束'),输出4. 结束。
- 遇到
- 检查微任务队列:
- 队列中有
Promise.then的回调。 - 执行回调,输出
3. Promise.then。
- 队列中有
- 执行宏任务队列:
- 队列中有
setTimeout的回调。 - 执行回调,输出
2. setTimeout。
- 队列中有
最终控制台输出结果:
1. 开始
4. 结束
3. Promise.then
2. setTimeout
案例二:嵌套任务与微任务的“陷阱”
这是一个更容易出错的复杂案例:
console.log('Script Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Promise inside Timeout');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('Timeout inside Promise');
}, 0);
});
console.log('Script End');
推演步骤:
- 执行同步代码:
- 输出
Script Start。 setTimeout回调 -> 宏任务队列(记为 宏1)。Promise.then回调 -> 微任务队列(记为 微1)。- 输出
Script End。
- 输出
- 检查微任务队列:
- 执行 微1 (
Promise 1)。 - 在微1内部,遇到
setTimeout,将其回调放入宏任务队列(记为 宏2)。 - 微任务队列此时已清空。
- 执行 微1 (
- 执行宏任务队列:
- 执行 宏1 (
Timeout 1)。 - 在宏1内部,遇到
Promise.then,将其回调放入微任务队列(记为 微2)。 - 宏1执行完毕。
- 执行 宏1 (
- 检查微任务队列(关键点:每次宏任务执行完后都要检查微任务):
- 队列中有 微2 (
Promise inside Timeout)。 - 执行 微2,输出
Promise inside Timeout。
- 队列中有 微2 (
- 执行宏任务队列:
- 执行 宏2 (
Timeout inside Promise)。 - 输出
Timeout inside Promise。
- 执行 宏2 (
最终控制台输出结果:
Script Start
Script End
Promise 1
Timeout 1
Promise inside Timeout
Timeout inside Promise
四、 关键点总结与避坑
为了确保你编写的异步代码逻辑正确,请务必遵守以下规则:
-
记住优先级
微任务的优先级永远高于宏任务。只要微任务队列中还有任务,事件循环就会一直处理微任务,而不会去处理宏任务。 -
区分 Promise 的状态变化
new Promise((resolve) => {...})构造函数中的代码是同步执行的。只有resolve()后,.then()中的回调才会进入微任务队列。console.log('A'); new Promise((resolve) => { console.log('B'); // 同步执行 resolve(); }).then(() => { console.log('C'); // 微任务 }); console.log('D'); // 输出顺序: A, B, D, C -
警惕宏任务中的微任务嵌套
如案例二所示,当一个宏任务正在执行时,它产生的微任务不会立即执行,必须等待当前宏任务执行完毕,事件循环回到“检查微任务”阶段时才会执行。

暂无评论,快来抢沙发吧!