事件循环(Event Loop)—— 深度解析
事件循环(Event Loop)是JavaScript的核心机制,控制代码执行顺序。其核心组件包括:调用栈(执行同步代码)、宏任务队列(如setTimeout)、微任务队列(如Promise.then)。执行顺序为:同步代码->微任务(全部清空)->渲染/rAF->下一个宏任务。关键点:微任务会阻塞渲染,await相当于微任务,setTimeout(0)并非立即执行。不同环境有差
事件循环(Event Loop)—— 深度解析
一、先看概念(最重要的要点)
- Call Stack(调用栈):执行同步代码的地方。JS 引擎在这里逐帧执行函数调用。
- 任务(Task / macrotask)队列:例如
setTimeout
、setInterval
、用户交互事件(click)、UI 事件、postMessage 等。每次从任务队列取一个任务执行(形成一个 macrotask)。 - 微任务(Microtask / job)队列:例如
Promise.then
/catch
、queueMicrotask
、MutationObserver
的回调(都是微任务)。微任务在当前 macrotask 执行结束后立刻运行,且会循环执行直到队列清空,再进入下一个 macrotask。 - 渲染(layout / paint):浏览器会在合适的时机在 macrotask / microtask 处理完后触发渲染。渲染前后还有
requestAnimationFrame
的回调点(rAF 在下一帧渲染前运行)。 - 关键结论:同步代码 ->(macrotask 中) -> 所有微任务(直到空) -> 渲染/rAF(若需要) -> 下一个 macrotask。
二、任务类型和常见来源(举例)
-
宏任务(macrotask)示例:
- 初始脚本(页面加载的 script)
setTimeout
/setInterval
- UI 事件(click、keydown)
- I/O 回调、postMessage(某些实现)
-
微任务(microtask)示例:
Promise.then
/Promise.catch
/Promise.finally
queueMicrotask(...)
MutationObserver
回调
-
渲染相关:
requestAnimationFrame
(rAF):在下一帧“绘制前”执行(在微任务之后、绘制之前)
-
Node 环境的特别项:
process.nextTick()
:Node 的专用队列,优先级高于 Promise microtasks(能在当前 tick 的末尾立刻执行,注意可导致 starvation)。setImmediate()
:libuv 的 check phase,行为与setTimeout(...,0)
在某些情形不同(和 I/O 有关)。
三、执行顺序(浏览器版)—— 一个简化时间线
[ 开始一个 macrotask(比如执行一个 script) ]
↓ 执行同步代码(Call Stack)
↓ macrotask 结束 → 执行微任务队列(microtasks)直到为空(Promise.then 等)
↓ 若需要,执行 rAF 回调(在渲染前)
↓ 浏览器执行 layout / paint(渲染)
↓ 进入下一个 macrotask(比如下一个 setTimeout 回调或事件处理)
注意:微任务是“会把控制权留在当前线程直到队列清空”的 —— 如果你不停新增微任务,会导致渲染和下一个 macrotask 被延迟(可能造成卡顿)。
四、典型示例与精确输出(请按顺序理解原因)
示例 A — setTimeout
vs Promise.then
console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
输出顺序:
start
end
promise
setTimeout
原因:setTimeout
的回调是 macrotask,Promise.then
的回调是 microtask;当前 macrotask(script)结束后先跑微任务,再跑下一个 macrotask(setTimeout)。
示例 B — requestAnimationFrame
、微任务 与 setTimeout
console.log('start');
requestAnimationFrame(() => console.log('rAF'));
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('timeout'), 0);
console.log('end');
常见输出(浏览器):
start
end
promise
rAF
timeout
原因:微任务在当前 macrotask 后立即执行;rAF 回调在下一个可用帧的“绘制前”执行(也就是在微任务之后、在渲染和绘制之前);setTimeout
在后面的 macrotask 中执行。
示例 C — async/await
与 Promise.then
的相对顺序
console.log('script start');
async function foo() {
console.log('foo start');
await null; // 等同于:Promise.resolve(null).then(...)
console.log('foo end');
}
foo();
Promise.resolve().then(() => console.log('promise callback'));
console.log('script end');
输出顺序(浏览器):
script start
foo start
script end
foo end
promise callback
原因:
await
会把后续代码拆成微任务(await
遇到时立即把“后续”排入 microtask)。foo()
中遇到await
时就把foo
的“后续”微任务排入队列(这个排入发生在执行到await
的那一刻)。- 之后全局的
Promise.resolve().then(...)
再被排入微任务队列(排在foo
的后面)。microtasks 是 FIFO,所以foo end
在promise callback
之前。
面试点:
await
不是把函数继续执行放入宏任务,而是微任务(和.then()
一样),只是语法更清晰。
示例 D — Node 环境:process.nextTick
vs Promise.then
在 Node 中:
console.log('start');
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('end');
Node 输出顺序:
start
end
nextTick
promise
原因:process.nextTick
属于 Node 的专用队列,它在本轮事件循环“结束前”优先于 Promise microtasks 执行(注意:process.nextTick
太多会阻塞后续 I/O)。
五、细节与面试常考点(要点总结)
- 微任务会“饿死”渲染/宏任务:如果微任务在执行过程中不断产生新的微任务,浏览器会一直清空微任务队列而延迟绘制和后续 macrotask,导致界面卡顿。
await
等价于把后续逻辑排到 microtask,因此await
后面的代码会在当前 macrotask 结束后、微任务阶段运行。setTimeout(fn, 0)
并非“立即”执行:它只是把回调加入任务队列,等待下一个 macrotask。浏览器还可能对连续短定时器进行最小间隔限制(clamp),且后台标签页会被 throttle。requestAnimationFrame
在下一帧的“绘制前”执行(可用于做动画前的 DOM 读取/写入)。rAF 与微任务的顺序:微任务先执行 -> rAF 回调(若存在) -> paint。- Node 的事件循环比浏览器更复杂(libuv 多阶段),且 Node 有
process.nextTick
与setImmediate
两个特殊 API(优先级和阶段不同)。 - MutationObserver 回调是微任务队列的一部分(常用于“异步 DOM 变更通知”)。
六、常见陷阱 & 优化建议
-
不要在微任务中放过重计算或循环创建微任务,会导致 UI 无法及时刷新。
-
如果需要让浏览器完成渲染再执行某个操作:
- 可以用
requestAnimationFrame
(在渲染前)配合setTimeout(...,0)
做“渲染后”的粗略保证;或使用两次 rAF:rAF(() => rAF(() => { /* 已完成一帧绘制 */ }))
(常见技巧,但细节依实现而异)。
- 可以用
-
调试顺序:用
console.log
标记顺序;Chrome DevTools 的 Performance 面板可以捕获帧、回调与主线程卡顿。 -
在 Node 中避免滥用
process.nextTick
(会阻塞 I/O);Promise microtasks 更“规范”。
更多推荐
所有评论(0)