你是否也听过这些问题:
setTimeout和Promise谁先执行?- 为什么
MutationObserver比setTimeout快?process.nextTick是什么,和Promise有啥区别?如果你还在背「先执行同步,再执行微任务,再执行宏任务」,那这篇一定要收藏:
咱不讲大话,直接用一堆真代码 + 控制台输出,看一次记一辈子!
Event Loop 是啥?先记一个结论
先记死
JS 是单线程的:
- 同一时刻只能干一件事
- 遇到异步,不是真的并行,而是排队等下次执行
这套机制就叫 Event Loop(事件循环)
执行顺序:
一个宏任务开始 -> 执行所有同步任务 -> 执行所有微任务 -> 执行下一个宏任务...
下面我给你一张图片,可能你现在还看不懂,但是你别急,待我慢慢道来,保你看完后心情舒畅。
关键概念:宏任务 & 微任务
先分清:
-
宏任务(MacroTask)
script整体执行setTimeoutsetIntervalsetImmediate(Node)
-
微任务(MicroTask)
Promise.thenprocess.nextTick(Node)MutationObserverqueueMicrotask
执行顺序:
同步任务 > 微任务 > 宏任务队列里的下一个宏任务
最经典示例:setTimeout vs Promise
先看
<script>
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('script end');
</script>
输出:
script start
script end
promise
setTimeout
解析:
-
script标签加载时,浏览器把整段<script>看作一个宏任务(MacroTask) -
开始执行宏任务:
- JS 引擎从上往下执行,进入调用栈。
-
遇到
console.log('script start'):- 属于同步任务,立即输出:
script start
- 属于同步任务,立即输出:
-
遇到
setTimeout:setTimeout的回调函数注册到宏任务队列中,等待当前宏任务(整个<script>)执行完后,排队执行。
-
遇到
Promise.resolve().then(...):Promise.resolve()立即返回一个已完成(fulfilled)的 Promise。.then()把回调函数注册到微任务队列中,等待本次宏任务(整个<script>)的同步任务执行完后,立刻执行。
-
执行
console.log('script end'):- 也是同步任务,立即输出:
script end
- 也是同步任务,立即输出:
-
当前宏任务(整个
<script>)的同步任务执行完毕,JS 引擎检查微任务队列:-
发现一个
Promise的.then回调,立即执行:- 输出:
promise
- 输出:
-
-
微任务清空后,Event Loop 检查宏任务队列:
-
发现之前的
setTimeout回调在队列里,拿出来执行:- 输出:
setTimeout
- 输出:
-
-
这次执行循环结束,调用栈清空,等待下一个 Event Loop 周期。
再去看看我一开始给的那张图,是不是一目了然啦? 但是光到这还远远不够,接下来我们再来一个复杂点的
多个 setTimeout + Promise 混搭
再加点料,一定要掌握
console.log('同步 Start');
const promise1 = Promise.resolve('First Promise');
const promise2 = Promise.resolve('Second Promise');
const promise3 = new Promise(resolve => {
console.log('Promise3 同步');
resolve('Third Promise');
});
setTimeout(() => {
setTimeout(() => {
console.log('下一把再相见');
}, 0);
const promise4 = Promise.resolve('Fourth Promise');
promise4.then(v => console.log(v));
}, 0);
setTimeout(() => {
console.log('下下一把再相见');
}, 0);
promise1.then(v => console.log(v));
promise2.then(v => console.log(v));
promise3.then(v => console.log(v));
console.log('同步 End');
输出顺序:
同步 Start
Promise3 同步
同步 End
First Promise
Second Promise
Third Promise
Fourth Promise
下下一把再相见
下一把再相见
拆开解释:
-
同步部分先跑完
console.log('同步 Start')立即执行,输出:同步 Start- 执行
Promise.resolve('First Promise')和Promise.resolve('Second Promise')只是创建已完成状态的 Promise,不输出内容。 - 遇到
new Promise,构造函数中的console.log('Promise3 同步')是同步执行,立即输出:Promise3 同步。 setTimeout注册到宏任务队列中,不立即执行。- 再遇到第二个
setTimeout,同样注册到宏任务队列中,等后面执行。 .then回调全部注册到微任务队列,暂时不执行。- 最后
console.log('同步 End')是同步,立即输出:同步 End。
-
同步结束,立刻执行微任务:
promise1promise2promise3-
当前宏任务(整个
<script>)的同步任务跑完后,JS 引擎检查微任务队列。 -
按顺序执行:
promise1的.then回调执行,输出:First Promisepromise2的.then回调执行,输出:Second Promisepromise3的.then回调执行,输出:Third Promise
-
当前微任务队列清空。
-
-
下一个宏任务是第一个
setTimeout,先跑外层,内部又注册了一个setTimeout(宏任务)和一个Promise(微任务)-
进入第一个
setTimeout的回调:- 里面又注册了一个新的
setTimeout,加入宏任务队列末尾。 - 创建
Promise.resolve('Fourth Promise'),立刻变成已完成状态。 .then回调注册到当前微任务队列中。
- 里面又注册了一个新的
-
-
微任务
Fourth Promise先执行-
第一个
setTimeout的同步回调结束后,检查有没有微任务。 -
发现有刚才
.then注册的微任务Fourth Promise:- 立刻执行,输出:
Fourth Promise
- 立刻执行,输出:
-
微任务队列清空后,继续执行下一个宏任务。
-
-
最后跑第二个
setTimeout和第一个setTimeout内嵌的setTimeout-
按照注册顺序执行:
- 先执行脚本最初注册的第二个
setTimeout,输出:下下一把再相见 - 再执行第一个
setTimeout里嵌套的那个setTimeout,输出:下一把再相见
- 先执行脚本最初注册的第二个
-
还有哪些微任务?微任务再来一个:MutationObserver
MutationObserver 常用于监听 DOM 变化,它的回调是微任务!
来看一段示例:
<script>
const target = document.createElement('div');
document.body.appendChild(target);
const observer = new MutationObserver(() => {
console.log('微任务: MutationObserver');
});
observer.observe(target, {
attributes: true,
childList: true,
});
// 触发 DOM 变化
target.setAttribute('data-id', '123');
target.appendChild(document.createElement('span'));
target.setAttribute('style', 'color: red;');
console.log('DOM 修改同步结束');
</script>
输出:
DOM 修改同步结束
微任务: MutationObserver
流程:
MutationObserver监听了target节点- 同步里改变属性、插入子元素
- 同步结束后,
MutationObserver回调执行(微任务阶段)
Node.js 里多了谁?process.nextTick
在 Node.js 里,process.nextTick 也属于微任务,优先级比 Promise 还高!
看这个示例:
console.log('Start');
process.nextTick(() => {
console.log('Process Next Tick');
});
Promise.resolve().then(() => {
console.log('Promise Resolve');
});
setTimeout(() => {
console.log('Timeout MacroTask');
}, 0);
console.log('End');
输出:
Start
End
Process Next Tick
Promise Resolve
Timeout MacroTask
执行顺序:
- 同步先跑完,输出
Start和End process.nextTick优先跑(比Promise还快)- 再跑
Promise的微任务 - 最后跑
setTimeout的宏任务
queueMicrotask 是干嘛的?
queueMicrotask 用来手动插入一个微任务
<script>
console.log('同步开始');
queueMicrotask(() => {
console.log('微任务: queueMicrotask');
});
console.log('同步结束');
</script>
输出:
同步开始
同步结束
微任务: queueMicrotask
跟 Promise.then 一样,都是「同步结束后补执行」。
总结!背这套口诀
✅ JS 是单线程,所有执行顺序:
同步任务 > 微任务 > 宏任务
✅ 常见微任务:
Promise.thenprocess.nextTick(Node)MutationObserverqueueMicrotask
✅ 常见宏任务:
- 整个
<script>文件本身 setTimeout/setIntervalsetImmediate(Node)I/O、MessageChannel、requestAnimationFrame(浏览器)
✅ 执行顺序永远是:
一个宏任务开始 -> 同步 -> 微任务 -> 下一个宏任务...
🪄 什么时候容易出 bug?
前端很多诡异的执行顺序:
- DOM 改了还没渲染完就测宽高,出错
- 定时器套 Promise,Promise 里套定时器,执行顺序全乱
- 多个
Promise+MutationObserver混用,队列顺序一不注意就懵
所以:
- 多用
console.log跑 demo - 看懂
Event Loop执行阶段,调试心里不慌
一句话总结
Event Loop = JS 的执行机制核心,搞懂了它,你就是异步的王者!