技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

宏任务和微任务的区别是什么

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/96-宏任务和微任务的区别是什么
本文目录12 个章节
  1. 1. 先区分同步与异步
  2. 2. task 与 microtask 不是进程和线程切换
  3. 3. 为什么计时器通常被称为 task?
  4. 4. 事件不一定都是异步 task
  5. 5. async/await 与 Promise job
  6. 6. microtask 能否访问外层上下文?
  7. 7. MutationObserver 与渲染不是同一类工作
  8. 8. 为什么微任务通常先于后续 task?
  9. 9. 浏览器与 Node.js 的边界
  10. 10. 选择 task 还是 microtask?
  11. 小结
  12. 参考资料

宏任务和微任务的区别是什么

Category(分类): JavaScript Status: 已整理

“宏任务”和“微任务”是前端文章中常见的说法。更准确的规范术语是浏览器 HTML Standard 中的 task、task source、microtask 和 event loop,而 ECMAScript 还定义 Promise jobs。它们描述的是宿主调度抽象,不是操作系统的进程、线程或纤程切换。

1. 先区分同步与异步

同步代码按照当前调用栈执行,前一条语句没有完成时,后一条语句不会开始。长时间同步计算会阻塞同一个 JavaScript agent 的输入和其他任务:

console.log('first')

for (let index = 0; index < 1e8; index += 1) {
  // 模拟长时间同步计算
}

console.log('second')

异步 API 通常会先注册回调或返回 Promise,稍后由宿主或 ECMAScript job 机制安排后续工作。异步不等于“不会阻塞”:回调真正开始执行后仍然会占用当前 JavaScript agent,回调中的长循环同样会阻塞页面或 Node.js 线程。

JavaScript 可以通过 Worker、Node.js worker_threads、子进程等方式创建额外的执行 agent,但这些能力来自宿主环境,不代表每一段 JavaScript 代码都自动并行。

2. task 与 microtask 不是进程和线程切换

原文中“进程切换肯定是宏任务”“线程切换是微任务”“微任务是纤程切换”等说法需要删除。task/microtask 与 OS 调度层没有这种一一对应关系:

概念规范/宿主层含义不是它
task(常称宏任务)event loop 从某个 task source 取出并运行的一段工作不等于进程切换
microtask(微任务)当前 event loop 的 microtask queue 中排空的短后续工作不等于线程/纤程切换
Promise jobECMAScript 的 Promise continuation 抽象不等于创建新线程
Worker message接收方 agent 中的一次事件任务不等于共享同一个调用栈

一个 agent 内的 microtask 通常仍在同一个 JavaScript 执行线程中运行。浏览器如何把 agent 映射到 OS 线程或进程,是实现细节;Worker 之间是否真正并行,也取决于宿主调度和硬件资源。

3. 为什么计时器通常被称为 task?

浏览器的 setTimeout 到期后,会把回调安排到计时器相关的 task source;Node.js 则由 libuv/event loop 组织。计时器的延迟是阈值,不是“到点立即执行”的实时保证:

const start = performance.now()

setTimeout(() => {
  console.log('elapsed:', performance.now() - start)
}, 0)

// 当前同步代码仍然会先执行;长任务会推迟计时器回调。
for (let index = 0; index < 1e7; index += 1) {
  // busy work
}

不能因为计时器由宿主管理,就断言它一定由另一个进程管理。浏览器和 Node.js 都可能在同一进程中完成计时器调度;文件系统、DNS 等部分 Node 工作才可能使用 libuv threadpool,网络 I/O 又有不同的非阻塞实现。

4. 事件不一定都是异步 task

来自用户输入、网络或浏览器调度的事件通常会在某个 task 中交付,但代码主动触发事件时可以同步执行:

const target = new EventTarget()

target.addEventListener('ready', () => {
  console.log('listener')
})

console.log('before')
target.dispatchEvent(new Event('ready'))
console.log('after')
// before、listener、after

Node.js 的 EventEmitter.emit() 也会同步调用监听器。不要把所有“事件”都解释成从另一个进程切换回 JavaScript 的宏任务。

5. async/await 与 Promise job

async function 本身仍然是函数,调用它会立即返回 Promise。第一次 await 之前的代码可以同步执行;遇到 await 后,表达式会被 Promise 规范化,async 函数的后续部分会在 Promise reaction job 中恢复:

async function demo() {
  console.log('before await')
  const value = await 42
  console.log('after await', value)
  return value
}

console.log('call')
void demo().then((value) => console.log('fulfilled', value))
console.log('after call')

常见输出顺序是:

call
before await
after call
after await 42
fulfilled 42

await 42 不应被解释成“用户可观察地创建了一个新的 Promise、切换了纤程或线程”。更准确的描述是:它通过 PromiseResolve 和后续 reaction job 安排 async 函数的恢复。

6. microtask 能否访问外层上下文?

microtask 和 task 都可以通过闭包访问创建回调时的词法环境。是否能访问变量,不由“宏/微”标签决定:

let value = 1

Promise.resolve().then(() => {
  console.log('microtask:', value)
})

setTimeout(() => {
  console.log('task:', value)
}, 0)

value = 2

两个回调通常都能读取到 2。如果回调跨越 Worker 或其他 realm,则要遵守消息传递、结构化克隆、Transferable 或 SharedArrayBuffer 的边界;这不是 task/microtask 本身造成的。

7. MutationObserver 与渲染不是同一类工作

MutationObserver 通知由 DOM 标准安排为可合并的 mutation-observer microtask:

const node = document.createElement('div')

const observer = new MutationObserver((records) => {
  console.log('mutations:', records.length)
})
observer.observe(node, { childList: true })

node.append('a')
node.append('b')

多个同步修改可能在一次通知中合并。不能把所有 Observer 都归为同一模型,也不能说“渲染是微任务”:布局、绘制和 requestAnimationFrame 属于浏览器的 rendering/update-the-rendering 流程,受渲染机会、刷新率、可见性和节流影响。

浏览器的概念时间线更接近:

task
→ microtask checkpoint(排空 Promise/queueMicrotask 等)
→ 可能的渲染更新和 requestAnimationFrame
→ 后续 task

浏览器不保证每个 task 后都绘制,也不保证 requestAnimationFrame 与计时器的相对顺序固定。

8. 为什么微任务通常先于后续 task?

当前同步 task 结束后,event loop 通常进行 microtask checkpoint,并持续执行直到队列为空:

console.log('sync')

queueMicrotask(() => {
  console.log('microtask 1')
  queueMicrotask(() => console.log('microtask 2'))
})

setTimeout(() => console.log('timer task'), 0)

常见浏览器结果是 syncmicrotask 1microtask 2timer task。这并不意味着 microtask 可以抢占当前同步代码,也不意味着所有已经存在的 task 都按同一全局 FIFO 重新排序。不同 task source 的选择由宿主决定。

Node.js 还要把 process.nextTick 与 Promise/queueMicrotask 分开:在 CommonJS 中 nextTick 通常先于 V8 microtask;ESM 顶层的执行本身已经处在 Promise job 中,观察到的顺序可能不同。

9. 浏览器与 Node.js 的边界

浏览器

脚本/输入/计时器等 task
→ microtask checkpoint
→ 可能渲染
→ 下一个 task

Node.js

当前回调返回
→ process.nextTick queue
→ V8 Promise/queueMicrotask queue
→ libuv 的后续阶段

Node.js 常见 libuv 阶段包括 timers、pending callbacks、poll、check、close callbacks。文件系统和部分 DNS 操作可能使用 threadpool,网络 I/O 通常由 event loop 与操作系统非阻塞机制处理。Node 20/libuv 1.45 之后 timers 与 poll 的安排也有变化,不能把一张旧版浏览器图当作所有 Node 版本的模型。

10. 选择 task 还是 microtask?

  • 需要在当前同步工作结束后进行很短的状态收尾:可以考虑 queueMicrotask 或 Promise continuation;
  • 需要等待浏览器让出当前轮次、避免连续占用调用栈:可以考虑计时器、MessageChannel、Scheduler API 或拆分任务;
  • 需要在下一次绘制前更新视觉数据:考虑 requestAnimationFrame
  • 需要大量 CPU 计算:优先拆分任务或放到 Worker,而不是制造无限 microtask;
  • Node.js 中需要快速安排回调时,要明确区分 process.nextTickqueueMicrotasksetImmediate 的语义及版本差异。

小结

  1. task/microtask 是宿主和 ECMAScript 的调度抽象,不是进程、线程或纤程切换。
  2. 计时器和外部事件通常通过 task 交付,但主动 dispatchEvent/EventEmitter.emit 可以同步执行。
  3. async/await 是 Promise continuation,不是迭代器加纤程的规范实现。
  4. MutationObserver 可以使用 microtask;布局、绘制和 rAF 属于渲染更新流程。
  5. task 与 microtask 都能访问闭包;真正的跨 agent 隔离来自 Worker/realm 和消息传递边界。
  6. 浏览器和 Node.js 的 event loop 需要分开学习。

参考资料

作者:甜酸混合物1226 原文链接:https://juejin.cn/post/6880787856353132552 来源:稀土掘金。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS