JavaScript 中的 Job Queue:Promise, Mutation Observer, QueueMicrotask 的执行顺序

各位编程爱好者们,大家好!

今天,我们将一同深入探索JavaScript世界中一个既核心又常常令人困惑的机制——Job Queue,也就是我们常说的微任务队列。特别地,我们将聚焦于Promise、Mutation Observer和queueMicrotask这三个在日常开发中扮演重要角色的微任务来源,剖析它们在事件循环中的执行顺序和内在逻辑。理解这些,是掌握JavaScript异步编程精髓,编写高性能、可预测代码的关键。

JavaScript的异步基石:单线程与事件循环

首先,让我们回顾一下JavaScript最基本的设计原则之一:它是单线程的。这意味着在任何给定的时刻,JavaScript引擎只能执行一个任务。这带来了简单性,但也引出了一个问题:如果一个任务需要长时间运行(比如网络请求、文件读取或复杂的计算),它将阻塞主线程,导致用户界面卡顿,应用无响应。

为了解决这个问题,JavaScript引入了异步编程模型,其核心便是“事件循环”(Event Loop)。事件循环是一种机制,它不断地检查是否有任务需要执行,并将它们安排到执行栈中。这个模型将任务分成了两大类:宏任务(Macrotasks,也常被称为Tasks)和微任务(Microtasks,也常被称为Jobs)。

理解这两种任务队列的区分,是理解Job Queue执行顺序的前提。

宏任务与微任务:两种调度队列的区分

想象一下JavaScript引擎有一个中央调度员。这个调度员手里拿着两份待办清单:一份是“今日要事清单”(宏任务队列),另一份是“紧急插队小纸条”(微任务队列)。

宏任务(Macrotask / Task)

宏任务是构成事件循环迭代的基本单元。每一次事件循环迭代,都会从宏任务队列中取出一个任务来执行。当这个宏任务执行完毕后,事件循环并不会立即去处理下一个宏任务,而是会先检查微任务队列。

常见的宏任务来源包括:

  • 整体script代码的执行:你的整个JavaScript文件本身就是一个宏任务。
  • setTimeout()setInterval() 的回调函数。
  • UI 渲染事件。
  • I/O 操作(如网络请求完成的回调)。
  • requestAnimationFrame() 的回调(虽然它与渲染紧密相关,但其调度方式使其行为更接近宏任务,尽管严格来说它有自己的调度阶段)。
  • MessageChannelpostMessage 的回调。

微任务(Microtask / Job)

微任务则拥有更高的优先级。当一个宏任务执行完毕后,JavaScript引擎会暂停,并检查并清空整个微任务队列,然后才会进行任何可能的UI渲染,并接着开始下一个宏任务的执行。这意味着,在一个宏任务执行期间产生的所有微任务,都会在当前宏任务结束后、下一个宏任务开始前被执行。

微任务的这种“立即性”是其核心特征,也是它比宏任务优先级高的根本原因。它允许我们在不中断当前渲染周期的情况下,对一些轻量级、高优先级的异步操作进行调度。

常见的微任务来源包括:

  • Promise.then(), .catch(), .finally() 回调函数。
  • MutationObserver 的回调函数。
  • queueMicrotask() 函数调度的回调函数。
  • async/await 中的 await 表达式后面的代码(因为 async/await 是基于 Promise 实现的语法糖)。

让我们通过一个表格来直观对比宏任务和微任务的差异:

特性 宏任务 (Macrotask/Task) 微任务 (Microtask/Job)
调度时机 在事件循环的每次迭代中,从宏任务队列中取出一个执行。 在当前宏任务执行完毕后,下一个宏任务开始前,清空所有微任务。
优先级 相对较低,每次迭代只执行一个。 相对较高,每次迭代会清空整个队列。
常见来源 script, setTimeout, setInterval, I/O, UI rendering, postMessage, MessageChannel Promise 回调 (.then(), .catch(), .finally()), MutationObserver 回调, queueMicrotask
影响渲染 宏任务执行后,浏览器可能会进行渲染。 微任务执行完毕后,浏览器才进行渲染(如果需要)。

理解这个基本模型后,我们就可以深入到微任务队列的具体成员了。

深入微任务队列:Job Queue的内部机制

ECMAScript规范将微任务称为“Jobs”。所有微任务,无论其来源如何(Promise、Mutation Observer或queueMicrotask),最终都会被添加到同一个“Job Queue”中。这个队列遵循先进先出(FIFO)的原则。

当JavaScript引擎执行完当前栈中的所有同步代码(即当前宏任务的主体部分)后,它会立即检查Job Queue。如果Job Queue不为空,它会逐个取出并执行其中的所有微任务,直到队列清空。只有当微任务队列彻底清空后,事件循环才会考虑下一个宏任务或者进行UI渲染。

这个机制保证了微任务的“原子性”:它们会在当前任务和下一个任务之间,作为一个不可分割的批次被处理。

让我们通过一个简单的例子来感受一下:

console.log('Script Start'); // 1. 同步执行

setTimeout(() => {
    console.log('setTimeout Callback'); // 4. 宏任务
}, 0);

Promise.resolve().then(() => {
    console.log('Promise Callback'); // 3. 微任务
});

queueMicrotask(() => {
    console.log('queueMicrotask Callback'); // 3. 微任务
});

console.log('Script End'); // 2. 同步执行

// 预期输出:
// Script Start
// Script End
// Promise Callback
// queueMicrotask Callback
// setTimeout Callback

分析:

  1. console.log('Script Start'):同步代码,立即执行。
  2. setTimeout():将其回调函数注册为一个宏任务,推入宏任务队列,等待下一个宏任务周期。
  3. Promise.resolve().then()Promise.resolve()会立即将Promise状态变为fulfilled,其.then()回调函数被注册为一个微任务,推入微任务队列。
  4. queueMicrotask():将其回调函数注册为一个微任务,推入微任务队列。
  5. console.log('Script End'):同步代码,立即执行。
  6. 此时,主线程的同步代码执行完毕,当前宏任务结束。JavaScript引擎检查微任务队列。
  7. 微任务队列中有 Promise CallbackqueueMicrotask Callback。它们将按照进入队列的顺序依次执行。
  8. 微任务队列清空。
  9. 事件循环进入下一个宏任务周期,从宏任务队列中取出 setTimeout Callback 并执行。

从这个例子中,我们可以清晰地看到微任务(Promise和queueMicrotask)在当前宏任务结束后、下一个宏任务开始前被优先执行的特性。

接下来,我们将逐一深入探讨这三个主要的微任务来源。

Promise:微任务队列的先行者

Promise 是 ES6 引入的异步编程解决方案,它代表一个异步操作的最终完成(或失败)及其结果值。Promise 极大地改善了回调地狱问题,使异步代码更易于阅读和管理。

当一个 Promise 的状态从 pending 变为 fulfilled(成功)或 rejected(失败)时,通过 .then(), .catch().finally() 方法注册的回调函数并不会立即执行,而是会被调度为微任务,推入 Job Queue。

Promise 的状态与微任务调度:

  • Pending(待定):初始状态,既不是成功也不是失败。
  • Fulfilled(已成功):意味着操作成功完成。当 Promise 变为 fulfilled 时,所有通过 .then() 注册的成功回调会被收集起来,并作为微任务被推入 Job Queue。
  • Rejected(已失败):意味着操作失败。当 Promise 变为 rejected 时,所有通过 .catch() 注册的失败回调会被收集起来,并作为微任务被推入 Job Queue。
  • Settled(已敲定):Promise 处于 fulfilled 或 rejected 状态时,即为 settled。.finally() 回调在 Promise 变为 settled 后,也会被调度为微任务。

代码示例:Promise 链与微任务

console.log('P1: Script Start');

new Promise(resolve => {
    console.log('P2: Promise Constructor'); // 2. 同步执行
    resolve('P3: Promise Resolved');
}).then(value => {
    console.log(value); // 4. 微任务
    return new Promise(resolve => {
        console.log('P4: Inner Promise Constructor'); // 5. 微任务 (在P3之后,但在then回调中)
        resolve('P5: Inner Promise Resolved');
    });
}).then(value => {
    console.log(value); // 6. 微任务
});

setTimeout(() => {
    console.log('P7: setTimeout Callback'); // 7. 宏任务
}, 0);

console.log('P8: Script End'); // 3. 同步执行

// 预期输出:
// P1: Script Start
// P2: Promise Constructor
// P8: Script End
// P3: Promise Resolved
// P4: Inner Promise Constructor
// P5: Inner Promise Resolved
// P7: setTimeout Callback

分析:

  1. P1: Script Start:同步执行。
  2. new Promise(...) 构造函数是同步执行的,所以 P2: Promise Constructor 立即打印。
  3. resolve('P3: Promise Resolved') 被调用,Promise 状态变为 fulfilled。其后的 .then() 回调被调度为一个微任务,进入微任务队列。
  4. P8: Script End:同步代码,立即执行。
  5. 当前宏任务(主脚本)执行完毕。事件循环检查微任务队列。
  6. 执行微任务队列中的第一个微任务:打印 P3: Promise Resolved。在这个微任务内部,又创建了一个新的 Promise。
  7. P4: Inner Promise Constructor 立即打印(因为 Promise 构造函数是同步的)。
  8. 内部 Promise 立即 resolve('P5: Inner Promise Resolved'),其 .then() 回调被调度为新的微任务,进入微任务队列的末尾。
  9. 继续执行微任务队列中的下一个微任务(此时是刚刚加入的那个):打印 P5: Inner Promise Resolved
  10. 微任务队列清空。
  11. 事件循环进入下一个宏任务周期,执行 P7: setTimeout Callback

这个例子清楚地展示了 Promise 链中,每个 .then() 的回调都是一个独立的微任务,它们会按顺序在当前宏任务结束后被执行。

Async/Await 与 Promise:

值得一提的是,async/await 是 Promise 的语法糖。当你在 async 函数中使用 await 关键字时,它会暂停当前 async 函数的执行,并等待 Promise 解析。await 表达式后面的代码实际上会被包装成一个 Promise 的 .then() 回调,因此它也遵循微任务的调度规则。

async function asyncFunction() {
    console.log('Async Function Start'); // 2. 同步执行
    await Promise.resolve(); // 暂停,将后续代码作为微任务
    console.log('Async Function After Await'); // 4. 微任务
}

console.log('Main Script Start'); // 1. 同步执行
asyncFunction();
console.log('Main Script End'); // 3. 同步执行

// 预期输出:
// Main Script Start
// Async Function Start
// Main Script End
// Async Function After Await

Mutation Observer:DOM变化的守护者

MutationObserver 是一个强大的 Web API,它允许我们监听 DOM 树的变化。它可以观察节点的添加/删除、属性的修改、文本内容的改变等。与旧的 Mutation Events 相比,MutationObserver 提供了更好的性能和更精细的控制。

MutationObserver 观察到 DOM 发生变化时,它不会立即执行回调函数。相反,它会收集所有发生的变化记录,并将其回调函数作为一个微任务推入 Job Queue。这意味着,即使在同一个宏任务中发生了多次 DOM 变化,MutationObserver 的回调也只会在当前宏任务结束时被触发一次,并且会接收到一个包含所有变化记录的列表。这种批处理机制优化了性能。

代码示例:Mutation Observer 与微任务

首先,我们需要一个 DOM 元素来观察:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mutation Observer Demo</title>
</head>
<body>
    <div id="target-element">Original Content</div>
    <script src="script.js"></script>
</body>
</html>

script.js 内容:

console.log('M1: Script Start');

const targetElement = document.getElementById('target-element');

// 创建一个 Mutation Observer 实例
const observer = new MutationObserver((mutationsList, observerInstance) => {
    console.log('M5: Mutation Observer Callback triggered!'); // 5. 微任务
    mutationsList.forEach(mutation => {
        console.log(`  Mutation Type: ${mutation.type}`);
        if (mutation.type === 'attributes') {
            console.log(`    Attribute changed: ${mutation.attributeName}, old value: ${mutation.oldValue}`);
        } else if (mutation.type === 'characterData') {
            console.log(`    Text changed, old value: ${mutation.oldValue}`);
        }
    });
    // 在 Mutation Observer 回调中触发 Promise
    Promise.resolve().then(() => {
        console.log('M6: Promise from Mutation Observer'); // 6. 微任务
    });
});

// 配置观察选项:观察属性和子节点变化
observer.observe(targetElement, {
    attributes: true,
    childList: true,
    characterData: true,
    attributeOldValue: true, // 需要获取旧属性值
    characterDataOldValue: true // 需要获取旧文本内容
});

console.log('M2: Observer configured'); // 2. 同步执行

// 立即修改 DOM,触发 Mutation Observer
targetElement.setAttribute('data-test', 'value1'); // 触发属性变化
targetElement.textContent = 'New Content'; // 触发文本变化

console.log('M3: DOM changes made'); // 3. 同步执行

Promise.resolve().then(() => {
    console.log('M4: Promise after DOM changes'); // 4. 微任务
});

setTimeout(() => {
    console.log('M7: setTimeout Callback'); // 7. 宏任务
}, 0);

console.log('M8: Script End'); // 3. 同步执行

// 预期输出:
// M1: Script Start
// M2: Observer configured
// M3: DOM changes made
// M8: Script End
// M4: Promise after DOM changes
// M5: Mutation Observer Callback triggered!
//   Mutation Type: attributes
//     Attribute changed: data-test, old value: null
//   Mutation Type: characterData
//     Text changed, old value: Original Content
// M6: Promise from Mutation Observer
// M7: setTimeout Callback

分析:

  1. M1: Script Start:同步执行。
  2. targetElement 获取,observer 实例创建并配置。M2: Observer configured 打印。
  3. targetElement.setAttribute()targetElement.textContent = ...:这些是同步的 DOM 操作。它们会立即改变 DOM,并触发 MutationObserver 收集变化记录。但此时回调函数还不会执行,它被调度为微任务。
  4. M3: DOM changes made 打印。
  5. Promise.resolve().then():其回调被调度为一个微任务,进入微任务队列。
  6. setTimeout():其回调被调度为一个宏任务,进入宏任务队列。
  7. M8: Script End:同步执行。
  8. 当前宏任务(主脚本)执行完毕。事件循环检查微任务队列。
  9. 微任务队列中的第一个微任务是 M4: Promise after DOM changes,打印。
  10. 微任务队列中的第二个微任务是 M5: Mutation Observer Callback triggered!,打印。它会批量处理之前所有的 DOM 变化。
  11. Mutation Observer 回调内部,又创建了一个 Promise,其回调 M6: Promise from Mutation Observer 再次被调度为微任务,并加入到当前微任务队列的末尾。
  12. 此时,微任务队列中还有 M6: Promise from Mutation Observer,它会被立即执行并打印。
  13. 微任务队列清空。
  14. 事件循环进入下一个宏任务周期,执行 M7: setTimeout Callback

这个例子清晰地展示了 MutationObserver 的回调也是微任务,并且它会与 Promise 微任务在同一个微任务队列中竞争执行。在同一个宏任务中,它们被触发的顺序决定了它们进入微任务队列的顺序。

queueMicrotask:显式控制微任务的利器

queueMicrotask() 是一个相对较新的 Web API,它提供了一种标准且直接的方式来显式地将一个函数调度为微任务。它的行为非常简单:将你传入的回调函数推送到当前微任务队列的末尾。

queueMicrotask 出现之前,开发者有时会利用 Promise.resolve().then(fn) 来实现类似的功能,即强制将一个函数作为微任务执行。queueMicrotask 的出现,使得这种意图更加明确,代码更简洁,并且避免了创建不必要的 Promise 实例。

何时使用 queueMicrotask

当你需要在一个任务完成后,但在浏览器进行下一次渲染或执行下一个宏任务之前,立即执行一些轻量级、非阻塞的逻辑时,queueMicrotask 是一个理想的选择。例如:

  • 在UI更新后,立即执行一些依赖于最新DOM状态的计算。
  • 在自定义组件中,需要在状态变化后,但在事件循环的当前周期内完成所有副作用,以确保一致性。
  • 需要将一些回调推迟到当前同步代码执行完毕,但又不想像 setTimeout(0) 那样等到下一个宏任务周期。

代码示例:queueMicrotask 与其他微任务的交互

console.log('Q1: Script Start');

setTimeout(() => {
    console.log('Q7: setTimeout Callback'); // 7. 宏任务
}, 0);

Promise.resolve().then(() => {
    console.log('Q4: Promise 1 Callback'); // 4. 微任务
    queueMicrotask(() => {
        console.log('Q5: queueMicrotask from Promise 1'); // 5. 微任务
    });
});

queueMicrotask(() => {
    console.log('Q3: queueMicrotask 1'); // 3. 微任务
});

const observerTarget = document.createElement('div');
document.body.appendChild(observerTarget); // 确保元素在DOM中
const observer = new MutationObserver(() => {
    console.log('Q6: Mutation Observer Callback'); // 6. 微任务
});
observer.observe(observerTarget, { attributes: true });

// 触发 Mutation Observer
observerTarget.setAttribute('id', 'temp-id'); // 触发属性变化

console.log('Q2: Script End'); // 2. 同步执行

// 预期输出:
// Q1: Script Start
// Q2: Script End
// Q3: queueMicrotask 1
// Q4: Promise 1 Callback
// Q5: queueMicrotask from Promise 1
// Q6: Mutation Observer Callback
// Q7: setTimeout Callback

分析:

  1. Q1: Script Start:同步执行。
  2. setTimeout():注册宏任务。
  3. Promise.resolve().then():注册微任务 Q4: Promise 1 Callback
  4. queueMicrotask(() => { console.log('Q3: queueMicrotask 1'); }):注册微任务 Q3: queueMicrotask 1
  5. MutationObserver 创建并观察 observerTarget
  6. observerTarget.setAttribute():同步修改 DOM,触发 MutationObserver 收集变化,并注册微任务 Q6: Mutation Observer Callback
  7. Q2: Script End:同步执行。
  8. 当前宏任务(主脚本)执行完毕。事件循环检查微任务队列。
  9. 此时微任务队列中的顺序是:Q3: queueMicrotask 1 -> Q4: Promise 1 Callback -> Q6: Mutation Observer Callback
    • 执行 Q3: queueMicrotask 1
    • 执行 Q4: Promise 1 Callback。在 Q4 的回调内部,又 queueMicrotask 了一个新的微任务 Q5: queueMicrotask from Promise 1。这个新的微任务会立即被添加到当前微任务队列的末尾。
    • 此时微任务队列的剩余顺序是:Q6: Mutation Observer Callback -> Q5: queueMicrotask from Promise 1
    • 执行 Q6: Mutation Observer Callback
    • 执行 Q5: queueMicrotask from Promise 1
  10. 微任务队列清空。
  11. 事件循环进入下一个宏任务周期,执行 Q7: setTimeout Callback

这个例子强调了所有微任务都共享同一个队列,并且新加入的微任务(即使是在当前正在执行的微任务中创建的)也会被添加到队列的末尾,并在当前微任务批次中被执行。

微任务队列的执行顺序:优先级与插队

前面我们已经多次强调,Promise 回调、Mutation Observer 回调和 queueMicrotask 调度的回调,它们都属于微任务,并被放入同一个 Job Queue。这个队列遵循严格的 先进先出(FIFO) 原则。

关键在于:它们何时被触发,以及何时被添加到 Job Queue。

在同一个宏任务的执行过程中:

  1. Promise 状态变为 fulfilled 或 rejected 时,其 .then()/.catch()/.finally() 回调会被调度为微任务,并加入 Job Queue。
  2. MutationObserver 观察到 DOM 变化时,其回调会被调度为微任务,并加入 Job Queue。
  3. 当调用 queueMicrotask(fn) 时,fn 会被调度为微任务,并加入 Job Queue。

它们之间并没有固定的“优先级”顺序,而是取决于它们被触发并进入队列的实际时机。

让我们通过一个更加复杂的综合示例来彻底理解这一点。

console.log('Z1: Script Start'); // 1. 同步

setTimeout(() => {
    console.log('Z8: setTimeout 0'); // 8. 宏任务
}, 0);

// Promise 1
Promise.resolve().then(() => {
    console.log('Z4: Promise 1'); // 4. 微任务
    queueMicrotask(() => {
        console.log('Z6: queueMicrotask from Promise 1'); // 6. 微任务
    });
});

// Mutation Observer
const target = document.createElement('div');
document.body.appendChild(target);
const observer = new MutationObserver(() => {
    console.log('Z7: Mutation Observer Callback'); // 7. 微任务
    Promise.resolve().then(() => {
        console.log('Z7.1: Promise from MO'); // 7.1. 微任务 (在Z7之后,但仍在当前微任务批次)
    });
});
observer.observe(target, { attributes: true });

// queueMicrotask 1
queueMicrotask(() => {
    console.log('Z3: queueMicrotask 1'); // 3. 微任务
    Promise.resolve().then(() => {
        console.log('Z5: Promise from queueMicrotask 1'); // 5. 微任务
    });
});

// 触发 Mutation Observer
target.setAttribute('data-attr', 'value'); // 触发 MO

console.log('Z2: Script End'); // 2. 同步

// 预期输出:
// Z1: Script Start
// Z2: Script End
// Z3: queueMicrotask 1
// Z4: Promise 1
// Z5: Promise from queueMicrotask 1
// Z6: queueMicrotask from Promise 1
// Z7: Mutation Observer Callback
// Z7.1: Promise from MO
// Z8: setTimeout 0

综合分析:

  1. Z1: Script Start:同步执行。
  2. setTimeout(() => { console.log('Z8: setTimeout 0'); }, 0);:注册宏任务 Z8 到宏任务队列。
  3. Promise.resolve().then(() => { console.log('Z4: Promise 1'); ... });:注册微任务 Z4 到微任务队列。
  4. MutationObserver 被创建并观察 target 元素。
  5. queueMicrotask(() => { console.log('Z3: queueMicrotask 1'); ... });:注册微任务 Z3 到微任务队列。
  6. target.setAttribute('data-attr', 'value');:同步修改 DOM。MutationObserver 观察到变化,其回调被调度为微任务 Z7,并加入微任务队列。
  7. Z2: Script End:同步执行。

此时,同步代码执行完毕。微任务队列中的初始顺序为:
[Z3: queueMicrotask 1, Z4: Promise 1, Z7: Mutation Observer Callback]

现在,事件循环开始清空微任务队列:

  • 执行 Z3: queueMicrotask 1:打印 Z3。在其内部,Promise.resolve().then(() => { console.log('Z5: Promise from queueMicrotask 1'); }); 又注册了一个新的微任务 Z5。这个 Z5 被添加到当前微任务队列的末尾。

    • 微任务队列变为: [Z4: Promise 1, Z7: Mutation Observer Callback, Z5: Promise from queueMicrotask 1]
  • 执行 Z4: Promise 1:打印 Z4。在其内部,queueMicrotask(() => { console.log('Z6: queueMicrotask from Promise 1'); }); 又注册了一个新的微任务 Z6。这个 Z6 被添加到当前微任务队列的末尾。

    • 微任务队列变为: [Z7: Mutation Observer Callback, Z5: Promise from queueMicrotask 1, Z6: queueMicrotask from Promise 1]
  • 执行 Z7: Mutation Observer Callback:打印 Z7。在其内部,Promise.resolve().then(() => { console.log('Z7.1: Promise from MO'); }); 又注册了一个新的微任务 Z7.1。这个 Z7.1 被添加到当前微任务队列的末尾。

    • 微任务队列变为: [Z5: Promise from queueMicrotask 1, Z6: queueMicrotask from Promise 1, Z7.1: Promise from MO]
  • 执行 Z5: Promise from queueMicrotask 1:打印 Z5

    • 微任务队列变为: [Z6: queueMicrotask from Promise 1, Z7.1: Promise from MO]
  • 执行 Z6: queueMicrotask from Promise 1:打印 Z6

    • 微任务队列变为: [Z7.1: Promise from MO]
  • 执行 Z7.1: Promise from MO:打印 Z7.1

    • 微任务队列变为: [] (空)

微任务队列清空。

最后,事件循环进入下一个宏任务周期:

  • 执行 Z8: setTimeout 0:打印 Z8

这个复杂示例完美地展示了:

  1. 所有微任务都共享一个队列。
  2. 它们进入队列的顺序由它们在宏任务中被触发的顺序决定。
  3. 在微任务执行过程中新产生的微任务,会立即加入到当前微任务队列的末尾,并会在当前微任务批次中被执行。

实战分析与常见误区

为什么理解微任务很重要?

  1. 避免竞态条件:精确控制代码执行时机,防止数据不一致。
  2. 优化性能和响应性:将非关键但需要快速响应的逻辑放入微任务,确保UI在宏任务之间及时更新,同时避免阻塞主线程。
  3. 理解 DOM 更新时机:微任务会在渲染之前执行,因此如果你在微任务中修改 DOM,这些修改可能会在下一次浏览器渲染时一起呈现,而不是在每次微任务中都触发重绘。
  4. 构建可预测的异步流:在大型应用中,清晰的异步模型是代码可维护性的基石。

setTimeout(fn, 0) 的区别

这是初学者常犯的错误。虽然 setTimeout(fn, 0) 看起来是“立即”执行,但它实际上是将 fn 调度为一个宏任务。这意味着它会在当前宏任务和所有微任务执行完毕后,等待下一个事件循环迭代才会被执行。

console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
queueMicrotask(() => console.log('queueMicrotask'));
console.log('end');

// 输出:
// start
// end
// Promise
// queueMicrotask
// setTimeout

这个输出再次印证了微任务的优先级高于宏任务。

Node.js 环境下的差异

虽然本文主要聚焦浏览器环境,但值得一提的是,Node.js 的事件循环与浏览器有些许不同。Node.js 有一个独特的 process.nextTick(),它在微任务队列中的优先级甚至高于 Promise 回调。这意味着 nextTick 回调会在所有 Promise 微任务之前执行。但 queueMicrotask 在 Node.js 中行为与浏览器一致,在 Promise 之后执行。

渲染时机

浏览器通常会在一个宏任务执行完毕,并且微任务队列被清空之后,检查是否有需要进行的 UI 渲染。这意味着,如果你在微任务中进行了多次 DOM 操作,这些操作可能会被批量化处理,然后在下一次渲染周期中一次性呈现,从而减少回流和重绘的次数,提升性能。

精要之处

至此,我们对JavaScript中Job Queue的运作机制及其核心成员——Promise、Mutation Observer和queueMicrotask有了全面的认识。理解微任务队列的统一性及其先进先出原则,对于编写高效、可预测的异步JavaScript代码至关重要。掌握这些知识,能帮助我们更好地把握代码的执行时机,避免异步编程中的常见陷阱。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注