浏览器事件循环机制
1. 为什么需要事件循环?
JavaScript 是单线程的。为了防止耗时任务(如网络请求)阻塞主线程,导致页面卡死,JS 采用了非阻塞的异步模型。
事件循环 (Event Loop) 就是实现这一模型的机制,它负责协调代码执行、事件处理和任务队列。
2. 核心组成部分
- 调用栈 (Call Stack): 执行同步代码的地方。当函数被调用时入栈,执行完出栈。
- Web APIs: 浏览器提供的能力(DOM, AJAX, setTimeout 等)。当异步操作开始时,会被移出调用栈,交给 Web APIs 处理。
- 任务队列 (Task Queue): 存放异步操作完成后的回调函数。
- 宏任务队列 (Macrotask Queue)
- 微任务队列 (Microtask Queue)
3. 事件循环流程 (非常重要)
- 执行栈选择最先进入队列的宏任务(通常是整体代码
script),执行其同步代码。
- 执行过程中,如果遇到异步操作,将其回调放入对应的宏任务队列或微任务队列。
- 当当前宏任务执行完毕(调用栈清空)后,立即检查微任务队列。
- 清空微任务队列:依次执行所有微任务,直到队列为空。(如果在执行微任务过程中产生了新的微任务,也会在当前循环中被执行,这可能导致无限循环阻塞)。
- UI 渲染:浏览器尝试进行页面渲染(如果需要)。
- 下一轮循环:从宏任务队列中取出下一个宏任务,回到步骤 1。
口诀:一宏一清微,渲染再轮回。
4. 宏任务 vs 微任务
| 类型 | 常见 API |
|---|
| 宏任务 (Macrotask) | script (整体代码), setTimeout, setInterval, setImmediate (Node), I/O, UI Rendering |
| 微任务 (Microtask) | Promise.then/catch/finally, process.nextTick (Node), MutationObserver, queueMicrotask |
5. 经典面试题解析
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
(注:process.nextTick 是 Node 环境特有,优先级高于 Promise)
浏览器环境简易版输出预测:
- 同步:
1, 7
- 微任务:
8
- 宏任务1 (setTimeout):
2, 4 -> 微任务: 5
- 宏任务2 (setTimeout):
9, 11 -> 微任务: 12
(Node 环境稍有不同,且 Node 版本不同表现也不同,面试通常以浏览器为主)。
6. Vue.nextTick 原理
Vue 的 DOM 更新是异步的。当你修改数据后,DOM 不会立即更新,而是开启一个队列。
Vue.nextTick 的原理就是利用微任务(优先使用 Promise.then,降级使用 MutationObserver 或 setImmediate, setTimeout),在 DOM 更新循环结束之后执行延迟回调。