浏览器事件循环
面试回答: JavaScript 在渲染主线程上按调用栈同步执行。定时器、网络和用户事件由浏览器处理,满足条件后把回调排入任务队列;当前任务结束时,浏览器会执行一次微任务检查点,持续清空 Promise、
queueMicrotask等微任务。随后浏览器才有机会执行渲染相关工作,再进入下一个任务。因此常用的理解顺序是“一个任务 → 清空微任务 → 可能渲染 → 下一个任务”,但渲染并不是每轮事件循环都必然发生。
1. 事件循环属于谁?
事件循环不是 ECMAScript 语言本身定义的语法能力,而是浏览器宿主环境用来协调以下工作的调度机制:
- JavaScript 调用栈;
- 定时器、网络请求和用户交互等浏览器能力;
- 任务队列与微任务队列;
- 样式计算、布局、绘制等渲染工作。
JavaScript 可以保持单线程的执行模型,同时把等待中的工作交给浏览器处理。异步操作完成并不等于回调立即执行,它只是获得了进入相应队列、等待主线程调度的资格。
2. 核心组成
调用栈(Call Stack)
同步代码执行的地方。函数调用时入栈,执行结束后出栈。同一时刻,主线程只能执行调用栈顶部的代码。
浏览器能力(Web APIs)
浏览器负责计时、网络、DOM 事件等工作。例如 setTimeout 会向浏览器注册计时器,而不是让回调在调用栈中等待。
任务队列(Task Queue)
规范通常使用 task;“宏任务”是前端语境中的常见叫法。常见任务来源包括:
- 首次执行的页面脚本;
setTimeout、setInterval回调;- 点击、输入等用户事件回调;
- 网络和消息事件回调。
浏览器可能维护多个任务队列,并根据任务来源和调度策略选择下一个任务,因此不能把所有任务简单理解成唯一的先进先出数组。
微任务队列(Microtask Queue)
常见微任务包括:
Promise.then/catch/finally;queueMicrotask;MutationObserver回调。
当前任务结束后,浏览器会执行微任务检查点,并持续处理微任务,直到队列为空。微任务执行期间新加入的微任务,也会在同一次检查点继续执行。
process.nextTick和setImmediate属于 Node.js 等环境,不应该混入浏览器事件循环题目中。
3. 一轮事件循环如何推进?
可以用下面的顺序建立心智模型:
- 浏览器选择并执行一个任务。
- 任务中的同步代码依次进入调用栈执行。
- 调用栈清空后,执行微任务检查点,直到微任务队列为空。
- 如果到达合适的渲染时机,浏览器执行动画回调和渲染流水线。
- 浏览器选择下一个任务,开始下一轮调度。
“可能渲染”很重要:浏览器通常会结合刷新率、页面是否可见以及是否有视觉更新来决定是否渲染,并不是执行完每个任务都固定绘制一次。
4. 经典执行顺序
执行过程如下:
- 当前脚本是一个任务,依次打印
1。 setTimeout注册计时器,回调稍后进入任务队列。Promise构造函数同步执行,打印4;.then回调进入微任务队列。- 继续执行同步代码,打印
6。 - 当前任务结束,清空微任务队列,打印
5。 - 定时器回调作为后续任务执行,打印
2,并产生新的 Promise 微任务。 - 定时器任务结束,再次清空微任务队列,打印
3。
最终输出:
关键不是记住结果,而是每次都区分:当前正在执行的同步代码、当前任务结束后的微任务,以及后续任务。
5. 微任务与页面渲染
微任务会在浏览器获得下一次渲染机会之前清空,因此大量连续微任务可能长期占用主线程:
每个微任务又创建下一个微任务,微任务队列始终无法清空,后续任务和页面渲染都会被推迟。这称为微任务饥饿。
因此,微任务并不是“越多越快”。需要主动让出主线程的长任务,不应该通过递归微任务无限拆分。
6. requestAnimationFrame 在哪里执行?
requestAnimationFrame(rAF)回调既不是普通任务,也不是微任务。浏览器准备绘制新一帧时,会在样式计算、布局和绘制之前调用本帧的 rAF 回调。
它适合修改下一帧需要展示的视觉状态:
需要注意:
- rAF 跟随浏览器的渲染节奏,不保证固定为 16.67ms;
- 页面进入后台时,rAF 通常会被暂停或明显降频;
- rAF 回调执行时间过长,同样会阻塞渲染并造成掉帧。
requestIdleCallback 则用于浏览器判断当前帧还有空闲预算时执行低优先级工作,两者的定位不同。
7. 高频追问
setTimeout(fn, 0) 会立即执行吗?
不会。0 表示达到最小延迟后,回调可以进入任务队列。它仍要等待当前任务、当前微任务以及排在前面的其他任务完成,还可能受到嵌套定时器最小延迟和后台页面节流影响。
为什么 Promise 回调通常早于 setTimeout?
不是因为 Promise 的计算速度更快,而是因为调度阶段不同:Promise 回调进入微任务队列,会在当前任务结束时执行;定时器回调属于后续任务。
修改 DOM 后可以立刻读到新值吗?
JavaScript 层面的 DOM 属性通常已经改变,但屏幕像素不一定已经更新。浏览器要等主线程交还控制权,并获得合适的渲染机会后,才会完成样式、布局和绘制。
Node.js 事件循环和浏览器一样吗?
不一样。两者都有任务与微任务概念,但 Node.js 还包含 timers、poll、check 等阶段,并有 process.nextTick 队列。做浏览器题时不应使用 Node 专属 API 推导结果。
Vue nextTick 与事件循环有什么关系?
Vue 会批量调度 DOM 更新,并让 nextTick 回调在更新完成后执行。其具体实现与版本有关,但理解微任务检查点有助于理解为什么同步修改状态后,DOM 不一定立刻完成更新。详见 Vue nextTick。
8. 总结
- 异步操作完成后,回调只是进入队列等待,并不会抢占当前调用栈。
- 一个任务结束后,浏览器会清空微任务,再获得处理渲染和后续任务的机会。
- 渲染不是普通任务,也不是每轮事件循环都必然发生。
- 分析输出题时,先写同步代码,再写本轮微任务,最后处理后续任务。

