事件循环机制(Event Loop)
在 JavaScript 里,事件循环(Event Loop)是一个经典的考点,同时也是很多前端开发者在面试或开发中容易混淆的概念。为什么 setTimeout(fn, 0) 不是立即执行的?为什么 Promise.then() 会比 setTimeout() 先执行?本文将深入解析 JavaScript 事件循环的工作原理,并通过真实案例帮助你彻底理解它。
一、什么是事件循环?
JavaScript 是单线程的,这意味着它一次只能执行一段代码。为了确保 UI 渲染不会被长时间阻塞,JavaScript 采用了一种事件驱动、异步非阻塞的执行模型——事件循环(Event Loop)。
事件循环的核心是:
同步代码 先执行(主线程)。
异步任务(如 setTimeout、Promise)会被放入不同的任务队列(宏任务或微任务)。
执行完主线程代码后,按顺序处理微任务队列(Microtask Queue)。
再执行宏任务队列(Macrotask Queue)。
重复以上步骤,形成循环。
二、任务队列:宏任务 vs. 微任务
在 JavaScript 中,异步任务分为宏任务(Macrotask)和微任务(Microtask),它们的执行顺序不同。
任务类型 具体 API
宏任务(Macrotask) setTimeout、setInterval、setImmediate(Node.js)
I/O 任务、UI 渲染、MessageChannel
微任务(Microtask) Promise.then、MutationObserver、queueMicrotask
执行顺序:
先执行所有同步任务,然后执行微任务,最后执行宏任务。
微任务的优先级高于宏任务。
三、事件循环的执行流程
示例 1:setTimeout vs Promise
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
1
2
3
4
执行顺序解析:
执行同步代码,输出 1。
setTimeout 进入 宏任务队列(不会立即执行)。
Promise.then() 进入 微任务队列。
继续执行同步代码,输出 4。
执行 微任务,输出 3。
事件循环进入下一个循环,执行 宏任务,输出 2。
最终输出结果:
1
4
3
2
示例 2:多个 Promise 与 setTimeout
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve()
.then(() => console.log("C"))
.then(() => console.log("D"));
console.log("E");
1
2
3
4
执行顺序解析:
console.log("A") → 同步任务,输出 A。
setTimeout 进入 宏任务队列。
Promise.then() 进入 微任务队列。
console.log("E") → 同步任务,输出 E。
执行 微任务:
console.log("C"),输出 C。
console.log("D"),输出 D。
事件循环进入下一个循环,执行 宏任务:
console.log("B"),输出 B。
最终输出结果:
A
E
C
D
B
四、Node.js 事件循环的不同之处
Node.js 也使用事件循环,但和浏览器不同:
Node.js 事件循环由 libuv 实现,包含多个阶段:
定时器阶段(Timers):执行 setTimeout 和 setInterval。
I/O 处理阶段:执行 I/O 相关的回调。
检查阶段(Check):执行 setImmediate 回调。
关闭回调阶段(Close Callbacks):处理 socket.on('close') 之类的回调。
在 Node.js 中,微任务(Promise、process.nextTick)会在每个阶段结束后立即执行,而不是等到整个主线程代码执行完毕后才执行。
示例 3:Node.js 的 setImmediate vs. setTimeout
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
在浏览器中,两者的执行顺序是固定的(setTimeout 进入宏任务)。
在 Node.js 中,执行顺序取决于代码执行时间:
如果 I/O 操作之前已经完成,setImmediate 先执行。
如果 setTimeout 的延迟足够小,可能 setTimeout 先执行。
五、常见面试题与解答
问题 1:下面代码的输出顺序?
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => {
console.log(3);
setTimeout(() => console.log(4), 0);
});
console.log(5);
1
2
3
4
5
6
7
8
9
10
执行顺序解析:
同步任务:
console.log(1),输出 1。
console.log(5),输出 5。
微任务队列:
Promise.then() 先执行,输出 3。
setTimeout(() => console.log(4), 0) 进入宏任务队列。
宏任务队列:
先执行 setTimeout(() => console.log(2), 0),输出 2。
之后执行 setTimeout(() => console.log(4), 0),输出 4。
最终输出:
1
5
3
2
4
六、如何利用事件循环优化前端性能?
1. 避免阻塞主线程
长时间的同步操作(如大数据计算、复杂 DOM 操作)会导致页面卡顿,可以使用:
Web Worker 处理计算任务。
requestIdleCallback 在浏览器空闲时执行任务。
2. 使用 requestAnimationFrame 进行流畅动画
function animate() {
requestAnimationFrame(animate);
console.log("动画帧");
}
requestAnimationFrame(animate);
它比 setTimeout(fn, 16) 更高效,因为它会在浏览器下一帧执行,减少不必要的计算。
3. 使用 queueMicrotask() 替代 setTimeout(fn, 0)
queueMicrotask() 属于微任务,比 setTimeout 执行更快:
queueMicrotask(() => console.log("microtask"));
setTimeout(() => console.log("macrotask"), 0);
1
2
输出:
microtask
macrotask
1
2
七、总结
JavaScript 采用单线程 + 事件循环模型,保证异步任务有序执行。
微任务优先级高于宏任务,如 Promise.then() 会先执行。
Node.js 事件循环不同,setImmediate() 可能比 setTimeout(fn, 0) 先执行。
优化前端性能:
避免阻塞主线程(Web Worker)。
使用 requestAnimationFrame 进行动画优化。
queueMicrotask 替代 setTimeout(fn, 0) 以提高性能

浙公网安备 33010602011771号