JavaScript 微任务与宏任务完全指南

张开发
2026/4/17 2:13:28 15 分钟阅读

分享文章

JavaScript 微任务与宏任务完全指南
JavaScript 微任务与宏任务完全指南前言先看这道经典面试题console.log(1);setTimeout((){console.log(2);},0);Promise.resolve().then((){console.log(3);});console.log(4);输出顺序是1 → 4 → 3 → 2而不是1 → 2 → 3 → 4。为什么setTimeout写了 0 秒延迟却排在最后为什么Promise比setTimeout先执行要搞懂这些必须理解 JavaScript 的**事件循环Event Loop**机制。一、先搞懂一个前提JavaScript 是单线程的JavaScript 只有一个线程一个工人 它一次只能做一件事想象一下你是一个厨师JS 引擎只有一双手 你不能同时炒两个菜 你只能做完一个再做下一个那问题来了如果遇到很耗时的任务比如网络请求难道要傻等吗答案是不等交给别人去做做完了通知我。这就引出了异步和事件循环。二、任务的三种分类JavaScript 中的代码分为三类┌─────────────────────────────────────────────┐ │ 所有任务 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 同步任务 │ │ 微任务 │ │ 宏任务 │ │ │ │ │ │ │ │ │ │ │ │ 立即执行 │ │ 插队VIP │ │ 排队普通 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────┘1. 同步任务立即执行console.log(1);// 同步consta12;// 同步console.log(4);// 同步遇到就立刻执行不等待。2. 微任务Microtask—— VIP 队列Promise.resolve().then((){...})// Promise 回调async/await// 本质也是 PromisequeueMicrotask((){...})// 手动添加微任务MutationObserver// DOM 变化监听3. 宏任务Macrotask—— 普通队列setTimeout((){...})// 定时器setInterval((){...})// 循环定时器setImmediate((){...})// Node.js 环境I/O操作// 文件读写、网络请求UI渲染// 浏览器页面渲染三、用餐厅比喻理解把 JavaScript 想象成一个只有一个服务员的餐厅 ┌─────────────────────────────────────────────────────┐ │ ️ 餐厅 │ │ │ │ ‍ 服务员JS 主线程一次只能服务一桌客人 │ │ │ │ ┌─────────────┐ │ │ │ 当前桌同步│ ← 正在服务的客人必须先搞定 │ │ └─────────────┘ │ │ │ │ ┌─────────────┐ │ │ │ VIP 队列 │ ← 微任务有 VIP 卡优先服务 │ │ │ 微任务 │ │ │ └─────────────┘ │ │ │ │ ┌─────────────┐ │ │ │ 普通队列 │ ← 宏任务普通客人VIP 之后再服务 │ │ │ 宏任务 │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────┘服务顺序1️⃣ 先把当前桌的客人全部服务完同步代码 2️⃣ 看看 VIP 队列有没有人微任务全部服务完 3️⃣ 再从普通队列叫一个人宏任务 4️⃣ 服务完这个人后再看 VIP 队列有没有新人 5️⃣ 重复 3-4 步...四、事件循环Event Loop机制核心规则┌──────────────────────────────────────────┐ │ 事件循环规则 │ │ │ │ 1. 执行所有同步代码调用栈清空 │ │ ↓ │ │ 2. 清空微任务队列全部执行完 │ │ ↓ │ │ 3. 取出一个宏任务执行 │ │ ↓ │ │ 4. 再次清空微任务队列 │ │ ↓ │ │ 5. 回到第 3 步循环往复... │ │ │ └──────────────────────────────────────────┘流程图┌─────────────┐ │ 开始执行代码 │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ 执行同步代码 │ ← 遇到异步就放到对应队列 └──────┬──────┘ │ ▼ ┌─────────────────┐ 有 ┌──────────────┐ │ 微任务队列有任务 │ ──────→ │ 执行所有微任务 │ └────────┬────────┘ └──────┬───────┘ │ 没有 │ │ ←────────────────────────┘ ▼ ┌─────────────────┐ 有 ┌──────────────┐ │ 宏任务队列有任务 │ ──────→ │ 执行一个宏任务 │ └────────┬────────┘ └──────┬───────┘ │ 没有 │ │ 回到检查微任务 ↑ ▼ ┌─────────────┐ │ 程序结束 │ └─────────────┘五、回到开头的代码逐行分析console.log(1);// ① 同步setTimeout((){// ② 宏任务 → 放入宏任务队列console.log(2);},0);Promise.resolve().then((){// ③ 微任务 → 放入微任务队列console.log(3);});console.log(4);// ④ 同步第一阶段执行所有同步代码代码从上到下执行 第①行console.log(1) → 同步 → 立即执行 → 输出 1 ✅ 第②行setTimeout(...) → 异步 → 放入【宏任务队列】 第③行Promise.then(...) → 异步 → 放入【微任务队列】 第④行console.log(4) → 同步 → 立即执行 → 输出 4 ✅ 此时输出1, 4 ┌────────────────────────────────────┐ │ 调用栈已清空 │ ├────────────────────────────────────┤ │ 微任务队列[() console.log(3)]│ ├────────────────────────────────────┤ │ 宏任务队列[() console.log(2)]│ └────────────────────────────────────┘第二阶段清空微任务队列微任务队列有任务 → 取出执行 执行() console.log(3) → 输出 3 ✅ 此时输出1, 4, 3 ┌────────────────────────────────────┐ │ 调用栈已清空 │ ├────────────────────────────────────┤ │ 微任务队列[] 已清空 │ ├────────────────────────────────────┤ │ 宏任务队列[() console.log(2)]│ └────────────────────────────────────┘第三阶段取出一个宏任务执行宏任务队列有任务 → 取出一个执行 执行() console.log(2) → 输出 2 ✅ 此时输出1, 4, 3, 2 ┌────────────────────────────────────┐ │ 调用栈已清空 │ ├────────────────────────────────────┤ │ 微任务队列[] │ ├────────────────────────────────────┤ │ 宏任务队列[] │ └────────────────────────────────────┘最终结果1 → 4 → 3 → 2六、为什么setTimeout(fn, 0)不是立即执行很多人的疑问我写的 0 毫秒啊为什么不立即执行setTimeout((){console.log(2);},0);// 0 毫秒0毫秒不代表立即执行而是尽快放入宏任务队列。setTimeout(fn, 0) 的意思 ❌ 不是0 毫秒后执行 fn ✅ 而是0 毫秒后把 fn 放入宏任务队列等轮到它再执行 就像你去银行取号 - 0 延迟 立刻拿到号 - 但你还得等前面的人同步代码 微任务办完七、更复杂的例子例1微任务中产生新的微任务console.log(1);setTimeout((){console.log(2);},0);Promise.resolve().then((){console.log(3);Promise.resolve().then((){console.log(4);});});console.log(5);分析过程第一阶段同步代码 → 输出 1 → setTimeout 放入宏任务队列 → Promise.then 放入微任务队列 → 输出 5 此时输出1, 5 第二阶段清空微任务队列 → 执行输出 3 → 执行过程中又产生了新的微任务输出4→ 放入微任务队列 → 微任务队列还没清空继续执行 → 执行输出 4 → 微任务队列清空了 此时输出1, 5, 3, 4 第三阶段宏任务 → 执行输出 2 最终输出1, 5, 3, 4, 2关键微任务执行过程中产生的新微任务也会在本轮全部执行完不会留到下一轮微任务就像 VIP 客人 VIP 在被服务时说我朋友也来了也是 VIP 服务员好的先生马上也服务他 而不是让他的朋友去普通队列排队例2宏任务和微任务交替console.log(1);setTimeout((){console.log(2);Promise.resolve().then((){console.log(3);});},0);setTimeout((){console.log(4);},0);Promise.resolve().then((){console.log(5);});console.log(6);分析过程┌─────────────────────────────────────────────────┐ │ 第一阶段同步代码 │ ├─────────────────────────────────────────────────┤ │ console.log(1) → 输出 1 │ │ setTimeout(输出2微任务) → 宏任务队列 │ │ setTimeout(输出4) → 宏任务队列 │ │ Promise.then(输出5) → 微任务队列 │ │ console.log(6) → 输出 6 │ │ │ │ 输出1, 6 │ │ 微任务队列[输出5] │ │ 宏任务队列[输出2微任务, 输出4] │ ├─────────────────────────────────────────────────┤ │ 第二阶段清空微任务 │ ├─────────────────────────────────────────────────┤ │ 执行输出 5 │ │ │ │ 输出1, 6, 5 │ │ 微任务队列[] │ │ 宏任务队列[输出2微任务, 输出4] │ ├─────────────────────────────────────────────────┤ │ 第三阶段取一个宏任务 │ ├─────────────────────────────────────────────────┤ │ 执行输出 2 │ │ 执行过程中Promise.then(输出3) → 放入微任务队列 │ │ │ │ 输出1, 6, 5, 2 │ │ 微任务队列[输出3] │ │ 宏任务队列[输出4] │ ├─────────────────────────────────────────────────┤ │ 第四阶段清空微任务宏任务执行后必检查 │ ├─────────────────────────────────────────────────┤ │ 执行输出 3 │ │ │ │ 输出1, 6, 5, 2, 3 │ │ 微任务队列[] │ │ 宏任务队列[输出4] │ ├─────────────────────────────────────────────────┤ │ 第五阶段取一个宏任务 │ ├─────────────────────────────────────────────────┤ │ 执行输出 4 │ │ │ │ 输出1, 6, 5, 2, 3, 4 │ └─────────────────────────────────────────────────┘最终输出1, 6, 5, 2, 3, 4例3async/await本质是微任务asyncfunctionfoo(){console.log(1);// 同步constresultawaitbar();// 等待 bar()之后的代码变成微任务console.log(2);// 微任务}asyncfunctionbar(){console.log(3);// 同步}console.log(4);foo();console.log(5);await到底干了什么// 这段代码asyncfunctionfoo(){console.log(1);awaitbar();console.log(2);}// 等价于functionfoo(){console.log(1);bar().then((){console.log(2);// await 后面的代码 .then() 里的回调 微任务});}执行过程console.log(4) → 输出 4同步 foo() → console.log(1) → 输出 1同步 → await bar() → console.log(3) → 输出 3同步bar 内部 → await 后面的代码放入微任务队列 console.log(5) → 输出 5同步 同步结束清空微任务 → console.log(2) → 输出 2 最终输出4, 1, 3, 5, 2八、速查表微任务 vs 宏任务 分类┌────────────────────────────────────────────┐ │ 微任务Microtask │ │ │ │ • Promise.then / catch / finally │ │ • async/awaitawait 之后的代码 │ │ • queueMicrotask() │ │ • MutationObserver │ │ • process.nextTick() ← Node.js 专属 │ │ │ ├────────────────────────────────────────────┤ │ 宏任务Macrotask │ │ │ │ • setTimeout / setInterval │ │ • setImmediate() ← Node.js 专属 │ │ • I/O 操作网络请求、文件读写 │ │ • UI 渲染 ← 浏览器专属 │ │ • requestAnimationFrame ← 浏览器专属 │ │ • 整段 script 代码第一个宏任务 │ │ │ └────────────────────────────────────────────┘执行优先级同步代码 微任务 宏任务 1 2 3 最高优先级 最低优先级事件循环口诀同步先走完 微任务清干净 宏任务取一个 微任务再清净 如此循环往复。九、做题模板遇到事件循环题按这个步骤分析第一步找出所有同步代码按顺序执行 → 遇到 setTimeout → 扔到宏任务队列 → 遇到 Promise.then → 扔到微任务队列 → 遇到 await → await 那一行是同步之后的代码是微任务 第二步同步代码执行完 → 清空微任务队列全部执行 第三步取一个宏任务执行 第四步执行完宏任务 → 清空微任务队列 第五步重复第三步十、总结┌─────────────────────────────────────────────┐ │ JavaScript 事件循环 │ │ │ │ 同步代码立即执行最高优先级 │ │ │ │ 微任务同步代码执行完后立即执行 │ │ → Promise.then │ │ → async/await 之后的代码 │ │ → 一次性全部清空包括执行中新产生的 │ │ │ │ 宏任务微任务清空后才执行 │ │ → setTimeout / setInterval │ │ → 一次只执行一个然后检查微任务 │ │ │ │ 执行顺序同步 → 微任务 → 宏任务 → 微任务 → … │ │ │ └─────────────────────────────────────────────┘一句话总结同步代码是正餐必须先吃完微任务是 VIP 插队优先宏任务是普通排队慢慢来。每服务完一个普通客人宏任务都要先看看有没有新的 VIP微任务。后记2026年4月16日13点37分于上海在opus 4.6辅助下完成。

更多文章