嵌套事件循环本质是任务队列的多层插入而非真实多个循环,执行顺序严格遵循“宏任务→清空微任务→下一宏任务”节奏,微任务可链式追加并本轮执行,宏任务则排队等待。

嵌套事件循环不是 JS 引擎里真实存在的“多个循环”,而是指在某个宏任务或微任务内部,又触发了新的宏任务(如 setTimeout)或微任务(如 Promise.then),从而形成多层任务调度。分析执行顺序的关键,不是数“几层循环”,而是严格遵循事件循环的三步节奏:执行当前宏任务 → 清空全部微任务 → 执行下一个宏任务 → 再清空全部微任务……如此反复。
先理清任务生成时机,再看插入队列位置
很多混乱源于混淆“代码执行”和“回调入队”。比如:
-
new Promise(resolve => { console.log('A'); resolve(); })中的console.log('A')是同步执行的,立刻输出; -
.then(() => console.log('B'))的回调是在 Promise 状态变为 fulfilled 后,被推入微任务队列,不是立刻执行; -
setTimeout(() => console.log('C'), 0)的回调是进入宏任务队列,哪怕延时为 0,也要等当前宏任务 + 所有微任务跑完才轮到。
微任务可以“链式嵌套”,但不会打断当前清空过程
在一个微任务中调用 Promise.resolve().then() 或 queueMicrotask(),新微任务会追加到当前微任务队列尾部,并在本轮清空阶段继续执行——它不会跳到下一个宏任务之后。
例如:
- 主脚本输出 '1';
- 遇到
Promise.then(() => { console.log('2'); Promise.resolve().then(() => console.log('3')); }),该回调入微任务队列; - 主脚本结束,开始清空微任务:先执行 '2',过程中又加入一个微任务 '3';
- 继续执行 '3',直到微任务队列真正为空;
- 才去取下一个宏任务(比如 setTimeout 回调)。
宏任务嵌套会排队,不越级
在 setTimeout 回调里再写一个 setTimeout,第二个回调会进入宏任务队列末尾,必须等前面所有宏任务(包括它自己所在这一轮的其他宏任务)都执行完后,才会被取出。
常见误区:
- 误以为
setTimeout(fn, 0)在微任务之后“马上”执行——其实它要等当前宏任务 + 所有微任务 + 可能已存在的其他宏任务(比如前一个 setTimeout)都走完; - 误以为
async/await会创建新事件循环——它只是语法糖,await后的代码会被包装成微任务,仍遵守“宏→微→宏”的节奏。
用真实例子验证逻辑链条
看这段典型嵌套代码:
console.log('start');<br>setTimeout(() => {<br> console.log('timeout1');<br> Promise.resolve().then(() => console.log('promise inside timeout'));<br>}, 0);<br>Promise.resolve().then(() => {<br> console.log('promise1');<br> setTimeout(() => console.log('timeout inside promise'), 0);<br>});<br>console.log('end');
执行顺序是:start → end → promise1 → timeout1 → promise inside timeout → timeout inside promise。
原因分步:
- 同步部分:'start'、'end' 立即输出;
- 微任务队列此时有 1 个(promise1 的回调),执行它,输出 'promise1',并把新的
setTimeout推入宏任务队列; - 微任务队列清空,取下一个宏任务:第一个
setTimeout,输出 'timeout1',并在其中加入一个新微任务; - 该微任务立即被加入队列,并在本轮宏任务结束后马上执行(输出 'promise inside timeout');
- 最后执行第二个
setTimeout(宏任务队列里排第二),输出 'timeout inside promise'。











