一文吃透:宏任务、微任务、事件循环、浏览器渲染、Vue 批处理与 Node 差异(含性能优化)

本文从一道面试题切入,系统梳理浏览器与 Node 的事件循环模型;解释宏任务与微任务的区别、执行顺序与常见坑;结合浏览器渲染时机;深入 Vue 3 批处理源码如何巧用微任务实现"同一轮内的合并更新";最后从任务调度角度讨论性能优化策略。完整成文,便于一次性阅读与复盘。

1. 从一道面试题入手:宏任务微任务混杂的打印顺序(浏览器)

示例代码(Chrome 环境;setImmediate 在浏览器一般不可用,若实现则顺序不稳定,不建议依赖):

js 复制代码
jsconsole.log(1);

setImmediate?.(() => {
  console.log(2);
});

setTimeout(() => {
  console.log(3);
  new Promise((resolve) => {
    console.log(10);
    resolve();
  }).then(() => {
    console.log(11);
  });
}, 0);

requestAnimationFrame?.(() => {
  console.log(4);
});

Promise.resolve().then(() => {
  console.log(5);
});

new Promise((resolve) => {
  resolve(6);
}).then(() => {
  console.log(7);
});

(async function () {
  console.log(8);
  await Promise.resolve();
  console.log(9);
})();

requestIdleCallback?.(() => {
  console.log(12);
}); 

常见输出(Chrome):

1, 8, 5, 7, 9, 4, 3, 10, 11, 12

顺序解析:

同步:1、8

微任务(在当前宏任务末尾清空):5、7、9

帧回调:requestAnimationFrame 在下一帧绘制前:4

计时器宏任务:setTimeout(0) 回调:3,回调内同步:10,回调末尾微任务:11

空闲回调:requestIdleCallback 优先级最低:12

2. 事件循环与宏/微任务:区别、原理,微任务会卡死吗?

同步任务:立即执行、阻塞线程。例:普通代码、同步 I/O、大量计算、浏览器强制布局。

宏任务(task):进入事件循环的任务队列,逐个取出执行。例:setTimeout/setInterval、用户事件、I/O 回调等。

微任务(microtask):优先级更高,在"当前宏任务结束后、下一个宏任务开始前"被"清空"。

浏览器:Promise.then/catch/finally、queueMicrotask、MutationObserver

Node:Promise 微任务 + process.nextTick(优先级高于 Promise 微任务)

事件循环关键节拍:

  1. 执行一个宏任务

  2. 清空当前产生的所有微任务(可能再产生微任务,继续清空直到无)

  3. 浏览器可能进入渲染阶段

  4. 进入下一轮宏任务

微任务会卡死吗?会。若在微任务里不断排入新的微任务(微任务"自旋"),规范会"清空"队列,导致渲染和后续宏任务都被饿死。Node 中滥用 process.nextTick 风险更高。

3. 浏览器渲染机制与宏任务、微任务

一帧(简化):

  1. 执行一个宏任务(如点击事件或计时器)

  2. 清空微任务

  3. 渲染步骤(样式计算 → 布局 → 绘制)

  4. 下一帧前执行 requestAnimationFrame

  5. 空闲阶段可能执行 requestIdleCallback(可带 deadline)

要点:

微任务在渲染前清空,因此框架能在"同一轮交互"内合并多次状态到一次 DOM 提交。

requestAnimationFrame 用于动画,回调在下一帧绘制前;回调内产生的微任务会在该帧绘制前被清空。

requestIdleCallback 优先级最低,适合非关键、可延迟任务,最好带超时兜底。

实务建议:

动画逻辑 → requestAnimationFrame

次要任务 → requestIdleCallback

微任务只做"收尾",避免自旋导致渲染饥饿

4. Node.js 的事件循环与浏览器区别

Node(libuv)阶段(简化):

  1. timers:到期的 setTimeout/setInterval

  2. pending callbacks:系统级 I/O 回调

  3. idle/prepare:内部使用

  4. poll:拉取 I/O 事件,大多数 I/O 回调在此执行;队列空且有 timer 到期会回到 timers;否则可能阻塞等待 I/O

  5. check:执行 setImmediate

  6. close callbacks: 事件等

  7. close

微任务:

process.nextTick:每个阶段切换前先清空,优先于 Promise 微任务

Promise 微任务:与浏览器类似,宏任务后清空

典型对比:

setTimeout(0) vs setImmediate:顶层脚本中通常 setTimeout(0) 先;在 I/O 回调中注册时往往 setImmediate 先。

Node 无 requestAnimationFrame/requestIdleCallback(那是浏览器渲染相关 API)。

把第 1 节示例裁剪为 Node(去掉 rAF/rIC)常见顺序:

1, 8, 5, 7, 9, 3, 10, 11, 2

5.从 Vue 批处理源码看宏/微任务

Vue 3 的"批处理(batching)"是把宏/微任务优势工程化的典型:

副作用函数(ReactiveEffect)包裹渲染与 watch,当依赖变更时不立即执行,而是交给 scheduler(job) 排队。

使用微任务 Promise.resolve().then(flushJobs) 统一触发 flush,把同一 tick 内多次状态变更合并为一次渲染与一次 DOM 提交。

多级队列与顺序:

pre 队列:flush: 'pre' 的 watch

主更新队列:组件更新 job(按 id 排序,保证父先子、稳定)

post 队列:flush: 'post'、onUpdated、指令后置等

Set 去重、执行计数与递归保护,防重复与无限循环。

简化伪码(核心思路):

ts 复制代码
const queue: Job[] = []
const queued = new Set<Job>()
let isFlushing = false, isFlushPending = false

function queueJob(job) {
  if (!queued.has(job)) {
    queued.add(job)
    queue.push(job)
    if (!isFlushing && !isFlushPending) {
      isFlushPending = true
      Promise.resolve().then(flushJobs) // 微任务触发批处理
    }
  }
}

function flushJobs() {
  isFlushPending = false
  isFlushing = true
  flushPreFlushCbs()
  queue.sort(jobComparatorById)
  for (const job of queue) job()
  queue.length = 0
  queued.clear()
  flushPostFlushCbs()
  isFlushing = false
} 

与渲染契合:

微任务清空发生在渲染之前,故框架可合并更新;nextTick() 等待这次 flush(含 post)完成,保证能读取到更新后的 DOM。

在 Node(SSR):

无浏览器渲染阶段,但同样利用微任务合并计算,避免重复渲染逻辑。

若业务滥用 process.nextTick 可能扰动 flush 节奏,需理解阶段优先级。

6. 从宏任务、微任务来看性能优化(浏览器与 Node)

切片长任务,避免长帧(>16.67ms)

使用 setTimeout/MessageChannel/requestIdleCallback 将大计算拆成小片

示例(MessageChannel 分片):

js 复制代码
const chan = new MessageChannel();
const BATCH = 1000;
function work(arr, i = 0) {
  const end = Math.min(i + BATCH, arr.length);
  for (let j = i; j < end; j++) { /* 计算 */ }
  if (end < arr.length) {
    chan.port1.postMessage(null);
    chan.port2.onmessage = () => work(arr, end);
  }
}
work(bigArray); 

动画与交互在正确时机

动画/视觉更新 → requestAnimationFrame

非关键、可延迟 → requestIdleCallback(带超时)

避免强制重排:读写分离,读在一起,写放 rAF

微任务只做"收尾",避免自旋

.then/queueMicrotask 用来合并与尾部处理,不要无限链式创建

框架已做批处理,业务少叠加微任务循环

Node:避免主线程被 CPU 任务绑死

用 worker_threads/子进程并行重计算;I/O 使用异步 API

合理选择 setImmediate(check 阶段尾部任务),谨慎 process.nextTick

限流与背压

浏览器:节流/防抖高频事件,虚拟列表/切片渲染

Node:流式处理(背压)、并发控制(p-limit/Bottleneck)

监控与诊断

浏览器:Performance 面板(Long Task、帧时间)、Lighthouse(TTI/TBT/LCP/CLS)

Node:perf_hooks.monitorEventLoopDelay()、APM、CPU/GC/堆快照

Vue 最佳实践

顺应批处理:同一 tick 内多次 state 修改会合并;读 DOM 用 nextTick

watch 的 flush 选择恰当;避免在 effect 中无界 setState 循环

任务安排"策略表":

关键视觉更新 → rAF

非关键低优先 → rIC(带 timeout)

批处理收束 → 微任务(适度)

重计算 → 切片/并行

I/O 密集 → 异步 + 背压

常见任务类型速查

同步(阻塞):普通 JS、复杂计算、大对象 JSON 处理、浏览器强制布局、Node 同步 I/O/加解密压缩

微任务:Promise.then/catch/finally、queueMicrotask、MutationObserver、Node process.nextTick

宏任务:setTimeout/setInterval、消息通道、浏览器事件回调、XHR/fetch 回调入队、requestAnimationFrame(渲染前)、requestIdleCallback(空闲)、Node I/O 回调(poll)、setImmediate(check)

总结

事件循环主线:一个宏任务 → 清空微任务 →(浏览器)渲染 → 下一轮宏任务。

浏览器侧:微任务在渲染前清空;requestAnimationFrame 在下一帧绘制前;requestIdleCallback 在空闲时。

Node 侧:libuv 分阶段;setTimeout 在 timers,setImmediate 在 check;process.nextTick 优先级最高。

Vue 3 批处理:用微任务统一 flush,多级队列(pre/更新/post)+ 去重排序,合并多次状态为一次 DOM 提交;nextTick 保证读到已更新 DOM。

性能优化要点:根据任务类型和渲染时机安排工作,切片/并行重任务,避免微任务饿死事件循环,利用框架批处理节奏。

相关推荐
狼性书生2 小时前
uniapp实现的Tab 选项卡组件模板
前端·uni-app·vue·组件·插件
吃饺子不吃馅2 小时前
前端画布类型编辑器项目,历史记录技术方案调研
前端·架构·github
程序猿追2 小时前
异腾910B NPU实战:vLLM模型深度测评与部署指南
运维·服务器·人工智能·机器学习·架构
拜晨2 小时前
使用motion实现小宇宙贴纸墙效果
前端·交互设计
拜晨2 小时前
使用motion实现小宇宙节目广场的效果
前端·交互设计
知花实央l3 小时前
【Web应用实战】 文件上传漏洞实战:Low/Medium/High三级绕过(一句话木马拿webshell全流程)
前端·学习·网络安全·安全架构
华仔啊3 小时前
JavaScript + Web Audio API 打造炫酷音乐可视化效果,让你的网页跟随音乐跳起来
前端·javascript
鸡吃丸子3 小时前
SEO入门
前端
檀越剑指大厂3 小时前
【Nginx系列】Tengine:基于 Nginx 的高性能 Web 服务器与反向代理服务器
服务器·前端·nginx