1. 事件循环总览
1.1 核心概念
事件循环是 JavaScript 的运行机制核心,负责在单线程模型下协调各种任务的执行顺序。它涉及到两个主要的队列:宏任务队列和微任务队列,以及浏览器或运行环境的渲染阶段。当主线程空闲时,事件循环会从宏任务队列取出一个任务执行,然后在该任务结束后清空微任务队列,再进入渲染阶段或继续执行下一个宏任务。
在这套机制中,执行顺序并不是简单地“先所有宏任务再所有微任务”,而是遵循一个循环:先执行一个宏任务,再执行它内部产生的微任务,直到微任务队列清空,然后才考虑页面渲染和下一轮循环。这一过程让微任务可以在一个宏任务结束后尽快完成,从而影响后续的输出和渲染。
2. 宏任务与微任务的定义
2.1 宏任务的来源与执行时机
宏任务包含诸如setTimeout、setInterval、I/O 回调、用户交互事件回调、渲染与重排、以及浏览器资源加载等任务。它们进入宏任务队列,按队列顺序逐个执行,每次执行一个宏任务后,都会进入微任务阶段来处理微任务。
在一个循环中,当一个宏任务被执行结束后,事件循环会触发<微任务队列刷新,这一步会依次执行所有已排队的微任务,直到队列为空。随后,如果需要,浏览器会进行渲染并进入下一轮宏任务的执行。这种结构确保了微任务在当前宏任务内尽可能提前完成,影响后续的渲染与输出。
console.log('脚本开始');
setTimeout(() => console.log('宏任务:timeout'), 0);
Promise.resolve().then(() => console.log('微任务:promise'));
console.log('脚本结束');
示例说明:脚本执行完毕后,输出的顺序通常是“脚本开始”、“脚本结束”、“微任务:promise”、“宏任务:timeout”。这体现了微任务在当前轮次结束前先于宏任务继续执行的特性。
3. 微任务的来源与执行时机
3.1 微任务的典型来源
微任务包括Promise 的回调(then、catch、finally)、queueMicrotask 提供的回调,以及某些环境中的 MutationObserver 等回调。它们通常在当前执行上下文的末尾被执行,优先级高于后续的宏任务。
你可以将微任务理解为“紧跟当前任务、在下一个渲染或宏任务前完成的工作”,它们的存在使得对状态的变更可以在单次事件循环中稳妥地生效,从而避免出现在同一轮循环中多次渲染的问题。
console.log('开始');
Promise.resolve().then(() => console.log('微任务:promise'));
queueMicrotask(() => console.log('微任务:queueMicrotask'));
console.log('结束');
输出顺序通常为:开始、结束、微任务:promise、微任务:queueMicrotask。这说明微任务会在当前脚本执行完成后、宏任务执行前被统一处理。

4. 事件循环中的执行顺序
4.1 宏任务与微任务的执行阶段
执行阶段遵循一个循环:从宏任务队列中取出一个任务执行;任务执行过程中可能产生新的微任务;执行完宏任务后,清空微任务队列,把所有微任务执行完毕;接着进行渲染(如果需要),再进入下一轮循环。这一流程决定了在同一轮循环中微任务的优先级高于后续的宏任务。
需要注意的是,浏览器的渲染时机并非总是严格在每轮宏任务之间进行;当微任务队列为空且页面处于空闲状态时,浏览器可能会在适当的时刻进行渲染,以提升用户体验。
console.log('S1');
setTimeout(() => console.log('A'), 0);
Promise.resolve().then(() => console.log('B'));
console.log('S2');
分析要点:输出顺序通常为 S1、S2、B、A。这里的微任务(Promise 回调)在当前轮次的宏任务“脚本执行结束”后先被执行,再进入下一轮的宏任务(setTimeout 回调)执行。
console.log('脚本开始');
setTimeout(() => console.log('外层 timeout'), 0);
Promise.resolve().then(() => console.log('内层微任务'));
setTimeout(() => Promise.resolve().then(() => console.log('内部微任务')) , 0);
console.log('脚本结束');
要点提示:即使在同一个宏任务内部,嵌套的微任务也会在该宏任务结束后立即执行,并且微任务会按照添加顺序逐个执行,直到队列清空,再进入下一轮宏任务。
5. 常见陷阱与边界情况
5.1 浏览器渲染时序与微任务的关系
渲染时序在事件循环的不同阶段会有不同的表现。微任务队列在当前宏任务结束后立即清空,随后才进入渲染阶段,意味着对界面的更新往往会被微任务的完成所影响。若微任务中包含大量计算或异步逻辑,可能会延迟渲染,产生页面卡顿的现象。
环境差异在浏览器和 Node.js 的事件循环实现上存在差异:浏览器把渲染和宏任务切分在不同的阶段,而 Node.js 更侧重于异步 I/O 与调度策略,因此理解目标执行环境的细节对排查问题很关键。
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('microtask'));
process.nextTick(() => console.log('nextTick'));
console.log('end');
要点总结:在 Node.js 环境中,process.nextTick 的执行优先级相对更高,会在微任务队列之前执行一次,影响整体的执行顺序;在浏览器环境下则以微任务优先于后续宏任务的规则为主。


