资讯动态

JavaScript 事件循环(Event Loop) 的运作流程(附:queueMicrotask() 将一个回调函数立即排队到微任务队列中)

发布时间:2026/8/23 2:46:27 来源:尧图企业网站定制
JavaScript通过事件循环机制实现异步操作核心在于调用栈、宏任务和微任务的协同工作。同步代码优先执行宏任务如setTimeout进入宏任务队列微任务如Promise.then进入微任务队列。每次事件循环先执行一个宏任务随后清空所有微任务队列再执行UI渲染浏览器环境。关键特点是微任务优先级高于宏任务且会立即全部执行完毕。典型执行顺序为同步代码→微任务→UI渲染→下一个宏任务。记忆口诀如下同步代码先跑完宏任务微任务排好队一次一个宏任务微任务插队宏任务后微任务清空重渲染下个宏任务接着搞。掌握这一机制可准确预测异步代码执行顺序。queueMicrotask() 将一个回调函数立即排队到微任务队列Microtask Queue中。JavaScript 事件循环(Event Loop) 的运作流程JavaScript 是单线程语言这意味着它同一时间只能执行一个任务。为了处理耗时操作如网络请求、定时器、文件读写而不阻塞主线程导致页面卡死JavaScript 引入了事件循环Event Loop机制。理解事件循环的核心在于掌握调用栈Call Stack、宏任务Macro Task和微任务Micro Task的执行顺序。1. 核心概念组件在深入流程前先了解三个关键角色组件作用特点调用栈 (Call Stack)执行同步代码的地方。后进先出 (LIFO)。函数调用入栈执行完毕出栈。栈清空代表同步代码执行完。宏任务队列 (Macrotask Queue)存放宏任务的队列。每次事件循环只取出一个宏任务执行。微任务队列 (Microtask Queue)存放微任务的队列。优先级更高。在当前宏任务结束后会一次性清空队列中所有微任务。2. 任务分类宏任务 vs 微任务正确区分任务是预测执行顺序的关键。 宏任务 (Macro Tasks)通常由宿主环境浏览器或 Node.js发起粒度较大。包含整体代码脚本 (script)setTimeoutsetIntervalI/O 操作 (文件读写、网络请求回调)UI 渲染 (浏览器特有通常在微任务后、下一个宏任务前)setImmediate(Node.js 特有)postMessage 微任务 (Micro Tasks)通常由 JavaScript 引擎内部发起粒度小优先级高。包含Promise.then/Promise.catch/Promise.finallyMutationObserver(监听 DOM 变化)queueMicrotask()process.nextTick(Node.js 特有优先级甚至高于其他微任务)注意async/await本质上是Promise的语法糖await后面的代码相当于放在Promise.then中因此属于微任务。3. 事件循环的详细运作流程事件循环是一个无限循环其标准执行步骤如下执行同步代码将整体脚本作为第一个宏任务推入调用栈。依次执行栈中的同步代码。遇到宏任务如setTimeout将其回调函数注册到宏任务队列。遇到微任务如Promise.then将其回调函数注册到微任务队列。调用栈清空当当前宏任务包括其内部所有同步代码执行完毕调用栈变为空。检查并执行微任务关键步骤事件循环检查微任务队列。如果队列不为空则依次取出所有微任务并推入调用栈执行直到微任务队列完全清空。注意如果在执行微任务过程中又产生了新的微任务它们也会被立即执行只要队列没空。UI 渲染浏览器环境微任务清空后浏览器可能会进行一次 UI 渲染更新页面视图。注这一步不是规范强制的但通常发生在宏任务之间。取出下一个宏任务从宏任务队列中取出最早进入的一个任务推入调用栈执行。重复循环回到步骤 2周而复始。4. 图解执行顺序[开始] ↓ 执行当前宏任务 (同步代码) ├─ 遇到宏任务 → 放入 [宏任务队列] └─ 遇到微任务 → 放入 [微任务队列] ↓ [当前宏任务执行完毕调用栈清空] ↓ 检查 [微任务队列] 是否为空 ├─ 否 → 依次执行所有微任务 (若产生新微任务则继续执行)直到清空 └─ 是 → 跳过 ↓ (可选) 浏览器进行 UI 渲染 ↓ 检查 [宏任务队列] 是否为空 ├─ 否 → 取出队首的一个宏任务执行回到上方 └─ 是 → 等待新任务 (空闲)5. 经典代码示例与解析让我们通过一段代码来验证上述流程console.log(1. 脚本开始 (同步)); setTimeout(() { console.log(2. setTimeout 回调 (宏任务)); Promise.resolve().then(() { console.log(3. setTimeout 内部的 Promise (微任务)); }); }, 0); Promise.resolve().then(() { console.log(4. 外层 Promise (微任务)); }); console.log(5. 脚本结束 (同步));执行推导过程同步执行打印1. 脚本开始 (同步)。遇到setTimeout将其回调包含日志2和3的逻辑放入宏任务队列。遇到Promise.then将其回调日志4放入微任务队列。打印5. 脚本结束 (同步)。同步代码执行完毕调用栈清空。检查微任务队列发现有一个微任务日志4。执行它打印4. 外层 Promise (微任务)。微任务队列清空。准备下一个宏任务从宏任务队列取出setTimeout的回调。执行该回调作为一个新的宏任务。打印2. setTimeout 回调 (宏任务)。在该回调内部遇到Promise.then将其回调日志3放入微任务队列。该宏任务内部同步代码执行完毕调用栈清空。再次检查微任务队列发现有一个微任务日志3。执行它打印3. setTimeout 内部的 Promise (微任务)。微任务队列清空。宏任务队列为空循环等待。最终输出顺序1. 脚本开始 (同步) 5. 脚本结束 (同步) 4. 外层 Promise (微任务) 2. setTimeout 回调 (宏任务) 3. setTimeout 内部的 Promise (微任务)6. 常见误区与注意事项微任务会“插队”微任务不是在“所有宏任务之后”执行而是在每一个宏任务执行完毕后、下一个宏任务开始前立即执行。这意味着如果一个宏任务内部不断产生微任务后续的宏任务会被一直推迟。UI 渲染的时机在浏览器中JS 执行完一个宏任务并清空微任务后浏览器有机会进行渲染。如果你在一个宏任务中修改了 DOM紧接着又在微任务中修改了 DOM浏览器通常只会渲染最后一次的状态因为微任务执行完才渲染这有助于性能优化。如果需要强制在微任务执行前渲染极少见可以使用requestAnimationFrame它通常被视为一种特殊的宏任务或在该阶段触发。Node.js 的特殊性Node.js 中process.nextTick的优先级高于Promise微任务。Node.js 的事件循环阶段Timers, Pending Callbacks, Poll, Check 等比浏览器更复杂但在“宏任务间隙清空微任务”这一核心规则上与浏览器一致。总结记住这句口诀同步代码先跑完宏任务微任务排好队一次一个宏任务微任务插队宏任务后微任务清空重渲染下个宏任务接着搞。掌握这个流程你就能准确预测任何异步 JavaScript 代码的执行顺序。queueMicrotask()queueMicrotask()是 JavaScript 中的一个全局方法用于将一个回调函数立即排队到微任务队列Microtask Queue中。它是现代 JavaScriptES2020 标准提供的一种更简洁、更直接的方式来调度微任务无需像以前那样必须通过Promise.resolve().then(...)来“曲线救国”。1. 核心作用当你调用queueMicrotask(callback)时立即排队callback函数会被放入当前的微任务队列。延迟执行它不会立即运行而是等到当前同步代码执行完毕且当前宏任务结束后在下一个宏任务开始之前执行。高优先级它的执行优先级高于setTimeout等宏任务与Promise.then处于同一梯队。2. 语法queueMicrotask(callback);callback: 当微任务队列被刷新时要执行的函数。该函数不接受任何参数也没有返回值返回值会被忽略。3. 与Promise.resolve().then()的对比在queueMicrotask出现之前开发者通常使用Promise来创建微任务特性queueMicrotask(fn)Promise.resolve().then(fn)语义清晰度高。明确表明“我要调度一个微任务”。低。看起来像是在处理异步结果实则是为了利用其微任务机制。性能开销略低。直接操作微任务队列无需创建 Promise 对象。略高。需要实例化一个 Promise 对象产生少量内存和计算开销。错误处理如果回调抛出错误会作为未捕获异常处理类似宏任务中的错误。可以通过.catch()优雅地捕获错误。兼容性现代浏览器 (Chrome 71, Firefox 69, Safari 12.1) 和 Node.js 11。所有支持 ES6 的环境。结论如果你只需要调度一个微任务而不涉及 Promise 链式调用或状态管理queueMicrotask是更优的选择。4. 代码示例场景在同步代码后、渲染前执行逻辑console.log(1. 同步代码开始); // 使用 queueMicrotask queueMicrotask(() { console.log(2. queueMicrotask 回调 (微任务)); }); // 等价于上面的 Promise 写法 Promise.resolve().then(() { console.log(3. Promise.then 回调 (微任务)); }); console.log(4. 同步代码结束); // 输出顺序 // 1. 同步代码开始 // 4. 同步代码结束 // 2. queueMicrotask 回调 (微任务) // 3. Promise.then 回调 (微任务)注意微任务队列是先进先出FIFO的所以queueMicrotask注册的回调会比后面注册的Promise.then先执行。5. 实际应用场景批量更新优化当你有多个地方可能触发同一个耗时的更新操作如重新计算布局、发送网络请求但你希望在一个事件循环中只执行一次let needsUpdate false; function scheduleUpdate() { if (!needsUpdate) { needsUpdate true; queueMicrotask(() { performHeavyUpdate(); // 只执行一次 needsUpdate false; }); } } // 无论调用多少次 scheduleUpdateperformHeavyUpdate 在本轮循环只跑一次 scheduleUpdate(); scheduleUpdate(); scheduleUpdate();库开发框架如 Vue, React或工具库内部需要在当前操作完成后立即清理副作用或通知观察者但又不想阻塞当前同步流程使用queueMicrotask比setTimeout更高效且时机更精准。避免竞态条件确保某些清理工作或状态同步在所有同步修改完成后立即发生防止中间状态被读取。6. 注意事项错误捕获如果queueMicrotask的回调函数中抛出错误它不会像 Promise 那样可以通过.catch()捕获。它会变成一个未捕获的异常通常会打印到控制台并可能终止脚本取决于环境。如果需要错误处理建议在回调内部使用try...catch。queueMicrotask(() { try { riskyOperation(); } catch (e) { handleError(e); } });执行时机它是在当前宏任务结束后立即执行。如果当前宏任务非常长微任务也会相应推迟。总结queueMicrotask()是 JavaScript 事件循环机制中的一把“瑞士军刀”它提供了一种轻量、语义明确的方式来安排高优先级的异步任务。对于现代前端开发尤其是涉及复杂状态管理和性能优化的场景它是一个非常有用的工具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价