Skip to content

JavaScript 宏任务与微任务详解

1. 基本概念

宏任务(MacroTask)和微任务(MicroTask)是 JavaScript 事件循环机制中的两个核心概念,用于管理异步任务的执行顺序。

1.1 宏任务

宏任务是由宿主环境(如浏览器、Node.js)发起的异步任务,具有以下特点:

  • 执行优先级低于微任务
  • 在下一轮事件循环中执行
  • 常见的宏任务包括:
    • setTimeout/setInterval
    • setImmediate(Node.js环境)
    • requestAnimationFrame(浏览器环境)
    • I/O操作
    • UI渲染事件
    • script标签中的整体代码
    • DOM事件回调

1.2 微任务

微任务是由 JavaScript 引擎自身发起的异步任务,具有以下特点:

  • 执行优先级高于宏任务
  • 在当前宏任务执行完毕后立即执行
  • 常见的微任务包括:
    • Promise.then/catch/finally
    • MutationObserver
    • process.nextTick(Node.js环境)
    • queueMicrotask
    • Object.observe(已废弃)

2. 执行机制与顺序

2.1 事件循环流程

JavaScript 引擎通过事件循环(Event Loop)来管理宏任务和微任务的执行,具体流程如下:

  1. 执行同步代码(属于宏任务)
  2. 执行所有微任务队列中的任务(清空微任务队列)
  3. 执行渲染操作(如果需要)
  4. 从宏任务队列中取出一个任务执行
  5. 重复步骤2-4

2.2 批量更新机制

批量更新是利用宏任务和微任务特性实现的一种优化策略,主要用于:

  • 减少不必要的DOM操作和渲染
  • 提高应用性能
  • 确保数据一致性

其核心思想是:将多次连续的更新操作合并为一次,在适当时机批量执行。

3. Vue源码中的应用

3.1 Vue异步更新队列实现原理

Vue 利用宏任务和微任务机制实现了高效的异步更新队列,主要通过 nextTick 方法实现:

3.1.1 核心实现逻辑

  1. 队列收集:当修改响应式数据时,Vue 会将需要更新的组件标记为脏,并将更新操作推入异步队列
  2. 异步执行:Vue 优先使用微任务(Promise.then),降级使用宏任务(setTimeout)
  3. 批量更新:多次修改数据只会触发一次实际的DOM更新

3.1.2 关键代码逻辑

javascript
// nextTick 核心实现逻辑
const callbacks = []
let pending = false

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// 优先使用微任务,降级使用宏任务
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 优先使用 Promise.then(微任务)
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {
  // 使用 MutationObserver(微任务)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 使用 setImmediate(宏任务)
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最后降级使用 setTimeout(宏任务)
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 支持 Promise 链式调用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

3.2 实际应用场景

在 Vue 中,异步更新队列的应用场景包括:

  • 数据修改后获取更新后的 DOM
  • 避免频繁 DOM 更新导致的性能问题
  • 确保数据驱动视图的一致性

4. 其他框架/库中的应用

4.1 React 中的应用

React 利用事件循环机制和微任务来处理状态更新和批处理:

  • React 的状态更新在同一个事件处理函数中会被合并(批量更新)
  • React 18 引入了自动批处理机制,在更多场景下自动合并更新
  • 使用 flushSync 可以强制同步执行更新(不推荐频繁使用)

4.2 Node.js 中的应用

Node.js 中的事件循环与浏览器略有不同,它将事件循环分为多个阶段:

  1. Timers 阶段:执行 setTimeout 和 setInterval 的回调
  2. Pending Callbacks 阶段:执行延迟的 I/O 回调
  3. Idle, Prepare 阶段:内部使用
  4. Poll 阶段:执行 I/O 回调
  5. Check 阶段:执行 setImmediate 的回调
  6. Close Callbacks 阶段:执行关闭事件的回调

在每个阶段执行完毕后,Node.js 会先清空所有微任务队列,然后再进入下一个阶段。

4.3 设计模式应用

宏任务和微任务的特性被广泛应用于以下设计模式:

  • 任务队列模式:实现异步任务的有序执行
  • 发布-订阅模式:确保订阅者在适当时机接收到通知
  • 批处理模式:合并多个操作以提高性能
  • 调度器模式:控制任务的执行时机和优先级

5. 实际应用技巧

5.1 性能优化策略

  • 使用微任务进行即时更新:对于需要在当前事件循环中完成的操作,优先使用微任务
  • 使用宏任务避免阻塞:对于耗时较长的操作,使用宏任务避免阻塞UI渲染
  • 合理利用批量更新:将多次DOM操作合并,减少渲染次数

5.2 常见应用场景

  • 数据获取与渲染分离:使用微任务确保数据完全获取后再渲染
  • 动画与交互优化:结合 requestAnimationFrame(宏任务)和微任务实现流畅动画
  • 错误处理与恢复:使用微任务处理异步操作中的错误,避免影响主流程
  • 复杂状态管理:利用任务队列管理复杂的状态更新依赖关系

6. 面试题与核心思想

6.1 经典面试题

面试题1:分析以下代码的输出顺序

javascript
console.log('1');

s setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => {
    console.log('3');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('4');
  setTimeout(() => {
    console.log('5');
  }, 0);
});

console.log('6');

答案:1 6 4 2 3 5

解析

  1. 先执行同步代码:输出 1 和 6
  2. 执行微任务:Promise.then 输出 4,并注册一个宏任务
  3. 执行宏任务:第一个 setTimeout 输出 2,并注册一个微任务
  4. 执行微任务:新注册的 Promise.then 输出 3
  5. 执行宏任务:第二个 setTimeout 输出 5

面试题2:Vue 中为什么要用 nextTick?

答案

  1. Vue 使用异步更新队列,当数据变化时,DOM 更新不会立即执行
  2. nextTick 可以在下次 DOM 更新循环结束后执行回调,确保能获取到更新后的 DOM
  3. 这是基于性能考虑,Vue 会将多次数据修改合并为一次 DOM 更新

面试题3:微任务和宏任务的区别是什么?

答案

  1. 执行时机:微任务在当前宏任务执行完毕后立即执行,宏任务在下一轮事件循环中执行
  2. 优先级:微任务优先级高于宏任务
  3. 来源:微任务由 JavaScript 引擎发起,宏任务由宿主环境发起
  4. 执行次数:每轮事件循环中,微任务队列会被清空,而宏任务队列只执行一个

6.2 核心思想记忆口诀

宏任务微任务执行顺序口诀

"同微宏,微先清,宏单执行再循环"

  • :先执行同步代码
  • :然后执行所有微任务
  • :接着执行宏任务
  • 微先清:微任务队列要全部清空
  • 宏单执行:宏任务每次只执行一个
  • 再循环:完成后进入下一轮循环

宏任务微任务分类口诀

"宏宿主,微引擎,认清单双要记清"

  • 宏宿主:宏任务由宿主环境(浏览器/Node)创建
  • 微引擎:微任务由 JavaScript 引擎创建
  • 认清单双:能分清哪些API属于宏任务,哪些属于微任务

Vue nextTick 实现口诀

"优先微,降级宏,队列收集批量更"

  • 优先微:优先使用微任务(Promise/MutationObserver)
  • 降级宏:降级使用宏任务(setImmediate/setTimeout)
  • 队列收集:将多个更新回调收集到队列中
  • 批量更:一次性批量执行所有更新

7. 总结

宏任务和微任务是 JavaScript 异步编程的核心概念,理解它们的执行机制对于编写高效、可靠的异步代码至关重要。通过合理利用宏任务和微任务,我们可以优化应用性能,改善用户体验,并避免常见的异步编程陷阱。

在实际开发中,应根据具体场景选择合适的任务类型:需要立即执行的操作使用微任务,需要延迟执行或避免阻塞的操作使用宏任务。同时,也要理解各框架如何利用这些机制来实现其核心功能,这有助于我们更好地使用这些框架并进行性能优化。

Updated at:

Released under the MIT License.