Event Loop(事件循环) 是 JavaScript 执行机制里的核心知识点。不管是前端面试还是日常开发,只要涉及异步,就绕不开它。
这篇文章不玩高深术语,用一套真实示例,带你搞懂:
- 同步任务 、宏任务 、微任务 到底是什么
- 它们的执行顺序和优先级
- 最后特别提醒:
Promise.resolve()
和.then()
到底哪个才是微任务?
🧵 为什么需要 Event Loop?
JS 是单线程,意味着同一时刻只能执行一件事。
比如:
js
console.log('A');
console.log('B');
console.log('C');
执行结果必然是:
css
A
B
C
这就是同步任务:顺序执行,执行完才往下走。
但是,现实开发中需要执行很多耗时任务(比如网络请求、定时器)。如果都用同步执行,页面直接卡死,体验没法要。
所以浏览器和 Node 设计了异步队列 + Event Loop,先把耗时任务挂起,空闲时再回头执行。
🗂️ 同步任务、宏任务、微任务,怎么分?
先看个对比表:
类型 | 代表 |
---|---|
同步任务 | 普通函数、console.log 、函数调用 |
宏任务(Macrotask) | 整个 <script> 、setTimeout 、setInterval 、setImmediate 、MessageChannel (浏览器)、I/O (Node) |
微任务(Microtask) | Promise.then() 、MutationObserver 、queueMicrotask 、process.nextTick (Node 专属) |
执行顺序口诀:
一个宏任务 → 执行同步任务 → 清空微任务 → 执行下一个宏任务 → 循环
✅ 浏览器执行顺序经典示例
来看个最典型的组合拳:
js
console.log('代码开始');
setTimeout(() => {
console.log('这是 setTimeout 回调');
}, 0);
Promise.resolve().then(() => {
console.log('这是 promise 的回调');
});
console.log('代码结束');
🧐 输出结果:
arduino
代码开始
代码结束
这是 promise 的回调
这是 setTimeout 回调
执行过程拆解:
<script>
整个文件就是一个宏任务,先跑同步代码。setTimeout
是下一个宏任务,排到队列后面。Promise.then
是微任务,会在本轮宏任务跑完后立刻执行。- 所以输出顺序是:同步 → 微任务 → 下一个宏任务
❗ 重点:Promise.resolve()
本身不是微任务!
超多人搞错,以为 Promise.resolve()
本身是微任务,其实不是!
来看个例子:
js
console.log('程序启动');
const p1 = Promise.resolve('第一个 Promise');
const p2 = Promise.resolve('第二个 Promise');
const p3 = new Promise(resolve => {
console.log('执行 p3 构造函数');
resolve('第三个 Promise');
});
p1.then(value => console.log(value));
p2.then(value => console.log(value));
p3.then(value => console.log(value));
setTimeout(() => {
console.log('第一波 setTimeout');
const p4 = Promise.resolve('第四个 Promise');
p4.then(value => console.log(value));
}, 0);
setTimeout(() => {
console.log('第二波 setTimeout');
}, 0);
console.log('程序结束');
执行顺序:
javascript
程序启动
执行 p3 构造函数
程序结束
第一个 Promise
第二个 Promise
第三个 Promise
第一波 setTimeout
第四个 Promise
第二波 setTimeout
🔑 为什么?
-
Promise.resolve()
只是返回一个已确定状态的 Promise,不会产生微任务。 -
真正触发微任务队列的是
.then()
的回调! -
所以这段里:
- 同步任务先跑完:
程序启动
→执行 p3 构造函数
→程序结束
- 再清空微任务队列:
第一个 Promise
、第二个 Promise
、第三个 Promise
- 然后执行第一个
setTimeout
(宏任务):第一波 setTimeout
,里面的.then()
会生成一个新的微任务:第四个 Promise
- 最后执行第二个
setTimeout
:第二波 setTimeout
- 同步任务先跑完:
🧬 MutationObserver:监听 DOM 改变的微任务
浏览器里,MutationObserver
也属于微任务。看个示例:
js
const target = document.createElement('div');
document.body.appendChild(target);
const observer = new MutationObserver(() => {
console.log('微任务: MutationObserver 回调执行');
});
observer.observe(target, { attributes: true, childList: true });
target.setAttribute('data-role', '123');
target.appendChild(document.createElement('span'));
这里修改了 target
的属性和子元素,MutationObserver
会把回调放进微任务队列,在本轮宏任务执行完后跑。
⚡ Node.js 专属:process.nextTick
比普通微任务还快!
Node.js 里还有个专属:process.nextTick
,它优先级比普通微任务还高。
js
console.log('启动');
process.nextTick(() => {
console.log('执行 process.nextTick');
});
Promise.resolve().then(() => {
console.log('执行 Promise.then');
});
setTimeout(() => {
console.log('执行 setTimeout');
Promise.resolve().then(() => {
console.log('setTimeout 中的 Promise');
});
}, 0);
console.log('收尾');
🧐 输出结果:
javascript
启动
收尾
执行 process.nextTick
执行 Promise.then
执行 setTimeout
setTimeout 中的 Promise
✏️ queueMicrotask:手动插个微任务
需要自己把任务插进微任务队列?用 queueMicrotask
!
js
console.log('准备执行');
queueMicrotask(() => {
console.log('这是 queueMicrotask 回调');
});
console.log('执行完同步任务');
输出:
准备执行
执行完同步任务
这是 queueMicrotask 回调
和 Promise.then
一样效果,都会在本轮宏任务执行完后立刻跑。
🗂️ 总结:执行顺序记住这张表
优先级 | 队列 |
---|---|
🥇 最高 | process.nextTick (Node 专属) |
🥈 第二 | Promise.then 、MutationObserver 、queueMicrotask |
🥉 最后 | 宏任务:setTimeout 、setInterval 、I/O |
顺序一定是:
- 当前宏任务执行完 → 清空所有微任务
- 再执行下一个宏任务
- 循环往复
🚀 最后的金句
Promise.resolve()
只是创建了一个已确定状态的 Promise,本身不是微任务。只有调用.then()
后,回调才会排进微任务队列!
别再搞混了!
面试题里很多顺序题,考点就是这里。