前端宏(微)任务 | 从“为什么我的代码不按顺序执行”说起

之前练习一个订单支付流程,写了这样一段代码:

js 复制代码
console.log('1. 开始支付')

Promise.resolve().then(() => {
  console.log('2. 支付成功回调')
})

setTimeout(() => {
  console.log('3. 超时检测')
}, 0)

console.log('4. 等待结果')

以为输出会是:

markdown 复制代码
1. 开始支付
2. 支付成功回调
3. 超时检测
4. 等待结果

但实际结果却是:

markdown 复制代码
1. 开始支付
4. 等待结果
2. 支付成功回调
3. 超时检测

"Promise 怎么跑到 setTimeout 后面去了? "------这正是 JavaScript 事件循环中最容易让人困惑的点:宏任务(Macrotask)和微任务(Microtask)的区别

今天我就用一个真实业务场景,带你彻底搞懂这两个概念。


一、问题场景:支付状态同步的"时序陷阱"

我们有个 H5 支付系统,流程如下:

  1. 调起微信支付 SDK
  2. 监听支付结果(通过回调)
  3. 支付成功后立即更新 UI
  4. 然后跳转到订单详情页

代码长这样:

js 复制代码
startWeChatPay(orderId).then(() => {
  // 支付成功
  updateOrderStatus('paid')     // 更新状态
  redirectToDetailPage()        // 跳转页面
})

// 同时注册一个全局超时检测
setTimeout(() => {
  if (order.status !== 'paid') {
    showTimeoutWarning()
  }
}, 1000)

但测试发现:有时页面还没来得及更新状态,就跳走了,用户看到的还是"待支付"界面。

为什么?因为 updateOrderStatus 里的某些异步操作被"延迟"了。要理解这个问题,我们必须深入事件循环内部。


二、解决方案:用微任务确保关键逻辑优先执行

我们把状态更新包装成一个微任务:

js 复制代码
startWeChatPay(orderId).then(() => {
  // 使用 queueMicrotask 确保优先执行
  queueMicrotask(() => {
    updateOrderStatus('paid')
    redirectToDetailPage()
  })
})

或者更常见的写法:

js 复制代码
startWeChatPay(orderId).then(async () => {
  await Promise.resolve() // 切入微任务队列
  updateOrderStatus('paid')
  redirectToDetailPage()
})

现在,updateOrderStatus 会在当前宏任务结束前立即执行,而不是等到下一个事件循环周期。


三、原理剖析:JavaScript 事件循环的双层调度机制

1. 表面用法:哪些是宏任务?哪些是微任务?

宏任务(Macrotask) 微任务(Microtask)
setTimeout Promise.then/catch/finally
setInterval queueMicrotask
setImmediate(Node.js) MutationObserver(浏览器)
I/O process.nextTick(Node.js)
script 标签整体代码

✅ 所有同步代码属于一个特殊的"初始宏任务"


2. 底层机制:事件循环如何调度?

我们来画一张 事件循环执行流程图

graph TD A[事件循环开始] --> B[取出一个宏任务执行] B --> C[执行过程中遇到:] C -->|宏任务| D[加入宏任务队列] C -->|微任务| E[加入微任务队列] D --> F[当前宏任务执行完毕] E --> F F --> G{检查微任务队列} G -->|有| H[逐个执行直到清空] G -->|无| I[进入下一阶段] H --> I I --> J[渲染更新:每帧一次] J --> A

🔍 关键点:每个宏任务执行完后,必须清空所有微任务,才能进入下一个宏任务


3. 用支付案例演示执行顺序

再看这段代码:

js 复制代码
console.log('1. 开始支付')                    // 同步 → 宏任务内

Promise.resolve().then(() => {
  console.log('2. 支付成功回调')             // 微任务
})

setTimeout(() => {
  console.log('3. 超时检测')                 // 宏任务(下一轮)
}, 0)

console.log('4. 等待结果')                    // 同步 → 宏任务内

执行步骤分解:

步骤 动作 当前输出
1 执行同步代码第1行 1. 开始支付
2 遇到 Promise → 微任务入队 ---
3 遇到 setTimeout → 宏任务入队 ---
4 执行同步代码第7行 4. 等待结果
5 当前宏任务结束 → 清空微任务队列 2. 支付成功回调
6 进入下一事件循环 → 执行 setTimeout 回调 3. 超时检测

这就是为什么 24 之后、3 之前执行。


四、设计哲学:为什么需要两种任务?

1. 宏任务:宏观控制流

  • 适合 UI 渲染、定时操作、I/O 等耗时较长的任务
  • 浏览器可以在两个宏任务之间进行渲染更新,保证流畅性

2. 微任务:微观精细控制

  • 适合需要"立即响应"的逻辑,如 Promise 链、状态同步
  • 确保异步操作的回调能尽快执行,避免中间被其他任务插入

💡 类比:

  • 宏任务 像"会议日程"------每天安排几件大事
  • 微任务 像"即时消息"------当前会议结束前必须处理完

五、对比主流异步方案

特性 宏任务(setTimeout) 微任务(Promise)
执行时机 下一个事件循环周期 当前宏任务末尾
延迟 至少 ~4ms 极低(<1ms)
是否阻塞渲染 否(中间可渲染) 是(清空微任务时)
适用场景 超时控制、节流防抖 异步链式调用、状态同步 ✅
风险 可能被延迟太久 过多微任务会导致页面卡顿

六、实战避坑指南

❌ 错误用法:在微任务中无限递归

js 复制代码
function bad() {
  Promise.resolve().then(bad) // 🔥 微任务永不结束,页面卡死
}

✅ 正确做法:拆分为宏任务

js 复制代码
function good() {
  setTimeout(good, 0) // 让出控制权,允许渲染
}

❌ 错误用法:依赖 setTimeout(0) 做精细时序控制

js 复制代码
// 你以为有先后?
setTimeout(step1, 0)
setTimeout(step2, 0)

✅ 正确做法:使用 Promise 链保证顺序

js 复制代码
Promise.resolve()
  .then(step1)
  .then(step2)

七、举一反三:三个变体场景实现思路

  1. 批量 DOM 更新优化

    将多个状态变更包裹在 queueMicrotask 中,利用微任务合并多次更新,在一次重排中完成。

  2. 防止微任务风暴

    对高频触发的微任务(如监听器),使用节流或降级为宏任务(setTimeout(fn, 0))。

  3. 跨框架状态同步

    在 Vue 的 $nextTick 和 React 的 flushSync 中,底层都依赖微任务机制确保视图与状态一致。


小结

宏任务和微任务不是两个独立系统,而是 JavaScript 事件循环的双层调度策略

  • 宏任务:控制整体流程节奏,允许渲染介入
  • 微任务:保证关键逻辑"原子性"执行,避免中间状态被破坏

记住这个口诀:

同步代码先执行,
微任务清到底,
渲染更新插个队,
宏任务接着来。

当你写 Promise.thenqueueMicrotask 时,你是在说:"这件事很重要,请在当前任务结束前立刻处理。"

而当你写 setTimeout(fn, 0) 时,你是在说:"这件事不急,请等下一回合再做。"

相关推荐
paopaokaka_luck4 分钟前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
患得患失94937 分钟前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_40 分钟前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路3 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔3 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang3 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔4 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任4 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴4 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔4 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js