JS循环机制
JavaScript 的事件循环 (Event Loop) 是其实现非阻塞异步编程的核心机制,尽管 JS 是单线程的。它通过一种高效的任务调度方式,处理定时器、网络请求、用户交互等异步任务。
事件循环中的关键角色:
角色 | 职责 | 常见类型 |
---|---|---|
调用栈 (Call Stack) | 执行同步代码,后进先出 (LIFO)。一旦开始执行直到栈空,才会处理其他任务。 | 同步执行的函数、语句 |
宏任务 (Macrotask) | 由浏览器或 Node.js 环境提供,每次事件循环通常只取一个执行。 | setTimeout , setInterval , setImmediate (Node), I/O 操作, UI 渲染, DOM 事件回调, script 整体代码 |
微任务 (Microtask) | 在当前宏任务结束后、下一个宏任务开始前立即执行,并且会清空整个微任务队列。优先级高于宏任务。 | Promise.then /catch /finally , MutationObserver , queueMicrotask , process.nextTick (Node) |
事件循环 (Event Loop) | 负责协调这些组件,不断检查调用栈和任务队列。 |
🔄 事件循环的工作流程
事件循环的运行机制可以概括为以下步骤:
- 执行同步代码:从宏任务队列中取出一个任务(最初是整体
script
代码)执行其中的同步代码,这会填入调用栈。 - 清空微任务队列:当前宏任务的同步代码执行完毕(调用栈空)后,事件循环会立即处理微任务队列,依次执行所有微任务。如果在执行微任务的过程中又产生了新的微任务,新微任务也会在当前轮次中被执行,直到微任务队列彻底清空。
- UI 渲染(如有需要):浏览器可能会在此处进行页面渲染(计算样式、布局、绘制等)。
- 取下一个宏任务:从宏任务队列中取出下一个宏任务执行,然后回到第 2 步,开始新一轮的循环。
📖 代码执行顺序实战
记住这个核心原则:同步代码 > 微任务 > 宏任务。
基础示例
console.log('1 - 开始'); // 同步setTimeout(() => {console.log('2 - 定时器回调'); // 宏任务
}, 0);Promise.resolve().then(() => {console.log('3 - Promise回调'); // 微任务
});console.log('4 - 结束'); // 同步// 输出顺序:
// 1 - 开始
// 4 - 结束
// 3 - Promise回调
// 2 - 定时器回调
过程分析:
- 执行整体代码(宏任务),输出同步的
1 - 开始
和4 - 结束
。 - 遇到
setTimeout
,其回调函数被放入宏任务队列。 - 遇到
Promise.then
,其回调函数被放入微任务队列。 - 当前宏任务执行完毕,开始清空微任务队列,输出
3 - Promise回调
。 - 微任务队列清空后,从宏任务队列中取出
setTimeout
的回调并执行,输出2 - 定时器回调
。
嵌套示例
console.log('Script start'); // 同步setTimeout(() => {console.log('setTimeout'); // 宏任务2Promise.resolve().then(() => {console.log('Promise inside setTimeout'); // 微任务3});
}, 0);Promise.resolve().then(() => {console.log('Promise1'); // 微任务1setTimeout(() => {console.log('setTimeout inside Promise'); // 宏任务3}, 0);
});console.log('Script end'); // 同步// 输出顺序:
// Script start
// Script end
// Promise1
// setTimeout
// Promise inside setTimeout
// setTimeout inside Promise
过程分析:
- 执行同步代码,输出
Script start
和Script end
。 - 清空微任务队列,执行
Promise1
的输出,并注册一个新的setTimeout
(宏任务3)。 - 执行下一个宏任务(
setTimeout
),输出setTimeout
,并在其内部将一个Promise.then
(微任务3)加入微任务队列。 - 当前宏任务(
setTimeout
)执行完毕后,再次清空微任务队列,输出Promise inside setTimeout
。 - 执行再下一个宏任务(
setTimeout inside Promise
),输出setTimeout inside Promise
。
⚠️ 注意事项与性能优化
- 避免阻塞主线程:长时间的同步任务会阻塞事件循环,导致页面无响应。对于复杂计算,可考虑使用 Web Workers 或将任务拆分成小块,利用
setTimeout
或requestAnimationFrame
分时执行。 - 警惕微任务风暴:在微任务中无限递归地添加微任务会导致事件循环永远无法进入下一个宏任务,页面会卡死。
// 错误示例:这将导致页面卡死 function infiniteMicrotask() {Promise.resolve().then(infiniteMicrotask); } infiniteMicrotask();
- 理解
async/await
:async/await
本质上是基于 Promise 的语法糖。await
语句之后的代码相当于被包装在Promise.then
中,因此是微任务。async function example() {console.log('Async function start');await Promise.resolve();console.log('After await'); // 这行代码是微任务! }
- 浏览器与 Node.js 的差异:虽然在现代浏览器和高版本 Node.js(v11+)中,事件循环的基本顺序(一个宏任务后清空所有微任务)已趋于一致,但 Node.js 的事件循环阶段划分更为复杂(如
setImmediate
和process.nextTick
的特有行为),在跨环境开发时需留意。
💎 总结
JavaScript 事件循环的机制可以概括为:
- 同步代码立即执行。
- 异步任务分类处理:微任务(如
Promise
)在当前宏任务结束后立即执行;宏任务(如setTimeout
)等待下一轮事件循环。 - 微任务优先级高于宏任务。
- 每个宏任务后都伴随着一次微任务队列的清空。
理解事件循环,能让你更从容地应对异步编程,写出更可靠、性能更好的代码。