前端中的事件循环(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。
- 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");
执行顺序解析:
- 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);
执行顺序解析:
- 同步任务:
- 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);
输出:
microtask macrotask
七、总结
- JavaScript 采用单线程 + 事件循环模型,保证异步任务有序执行。
- 微任务优先级高于宏任务,如 Promise.then() 会先执行。
- Node.js 事件循环不同,setImmediate() 可能比 setTimeout(fn, 0) 先执行。
- 优化前端性能:
- 避免阻塞主线程(Web Worker)。
- 使用 requestAnimationFrame 进行动画优化。
- queueMicrotask 替代 setTimeout(fn, 0) 以提高性能。
希望通过这篇文章,你能彻底掌握事件循环,在面试和实战中都能自信应对!🚀
- 同步任务: