当前位置: 首页 > news >正文

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 秒延迟,却排在最后?
为什么PromisesetTimeout先执行?

要搞懂这些,必须理解 JavaScript 的**事件循环(Event Loop)**机制。


一、先搞懂一个前提:JavaScript 是单线程的

JavaScript 只有一个线程(一个工人) 它一次只能做一件事

想象一下:

你是一个厨师(JS 引擎),只有一双手 你不能同时炒两个菜 你只能做完一个,再做下一个

那问题来了:如果遇到很耗时的任务(比如网络请求),难道要傻等吗?

答案是:不等!交给别人去做,做完了通知我。

这就引出了异步事件循环


二、任务的三种分类

JavaScript 中的代码分为三类:

┌─────────────────────────────────────────────┐ │ 所有任务 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 同步任务 │ │ 微任务 │ │ 宏任务 │ │ │ │ │ │ │ │ │ │ │ │ 立即执行 │ │ 插队VIP │ │ 排队普通 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────┘

1. 同步任务(立即执行)

console.log("1");// 同步consta=1+2;// 同步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


例3:async/await(本质是微任务)

asyncfunctionfoo(){console.log('1');// 同步constresult=awaitbar();// 等待 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/await(await 之后的代码) │ │ • 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辅助下完成。

http://www.jsqmd.com/news/653152/

相关文章:

  • 敏捷开发失效了?2026年新方法论探索
  • 做中后台业务,为什么我不建议你用 Tailwind CSS?
  • 初次使用降AI工具的完整入门教程:从零开始用嘎嘎降AI达标
  • Android应用如何精准识别并屏蔽主流模拟器运行环境
  • 哔哩下载姬DownKyi:如何免费解锁B站全画质视频下载的终极方案
  • AI客服机器人爆发前夜,你还在用2023版对话引擎?——2026奇点大会6项强制合规新规倒计时47天
  • 下一代软件:告别 GUI,CLI 底层革命
  • 图解 RAG:为什么大模型需要外挂知识库
  • 【JVM深度解析】第01篇:JVM前世今生与技术架构全景
  • 5G NR调度器:从帧结构到资源分配的实战解析
  • Cadence Virtuoso导入TSMC 65nm PDK保姆级避坑指南:从解压到仿真成功全流程
  • 2026 年两款服务器面板内存占用测试:宝塔面板和 1Panel 表现如何
  • GB/T 13123-2026 竹胶合板检测
  • 免费论文AIGC检测使用指南:原理实操全攻略
  • 扫普通链接二维码打开小程序页面参数获取
  • 开发者面试内卷:突出重围的差异化战术
  • 实战解析 | Workbench多单元混合建模在静力学分析中的高效应用
  • 当AI学会害怕和好奇——V4认知与情绪
  • 五大Web GIS地图框架深度对比:Leaflet、OpenLayers、Mapbox、Cesium与ArcGIS for JavaScript
  • 多益网络笔试里的Python哲学题怎么答?‘Explicit is better than implicit’对新手程序员意味着什么?
  • Cursor Pro激活技术深度解析:3大核心技术实现与实战指南
  • 如何用Jasminum插件3分钟搞定中文文献管理:Zotero终极效率提升指南
  • 【JVM深度解析】第02篇:类加载机制深度解析
  • DelphiZXingQRCode 实战:从零到一构建企业级二维码生成模块
  • OpenClaw Windows 一键部署全流程|解压即装+环境免配置,龙虾AI智能体本地快速落地
  • openEuler 22.03下5分钟搞定Docker安装与镜像加速(华为云镜像源实测)
  • 避开Matlab新手必踩的坑:空值判断的正确姿势(为什么a==[]永远返回false)
  • Bring up
  • 家庭网络搭建指南:从光猫到路由器的全流程解析
  • 将小龙虾接入ClawBot教程,用微信就能出电影解说视频