广告

JavaScript 中的宏任务与微任务到底是什么?执行顺序全解析与实战要点

1. JavaScript 中的宏任务与微任务到底是什么?

1.1 宏任务的定义

JavaScript 的执行模型 中,宏任务代表一个完整的任务周期,通常会在事件循环的一个轮次中执行完毕。它包含诸如 setTimeoutsetInterval、I/O 操作、以及浏览器的渲染阶段等耗时或需要等待外部事件的操作。理解这一点可以帮助我们把握何时会进入下一个循环。

当你在同步代码执行完成后,事件循环会从 宏任务队列中取出一个任务执行,直到该队列为空或达到某个资源限制为止。这个过程决定了后续代码的执行时机,尤其是在涉及异步回调时的行为。

// 宏任务示例
setTimeout(() => {console.log('宏任务执行');
}, 0);

要点提示:宏任务的触发通常来自浏览器的计时器、I/O 完成、或渲染阶段的队列等,是事件循环中的“外部任务”入口。

1.2 微任务的定义

与宏任务相对,微任务是在当前宏任务执行完成后、进入下一个宏任务执行前立即执行的任务。它们的目标是让某些操作在同一轮事件循环中尽快完成,以便为下一轮渲染或下一轮宏任务做准备。

微任务的典型来源包括 Promise.thenqueueMicrotask、以及 MutationObserver 等机制。需要强调的是,微任务队列在每个宏任务结束时会被清空,确保在进入渲染阶段前没有悬而未决的微任务。

Promise.resolve().then(() => console.log('微任务 1'));
Promise.resolve().then(() => console.log('微任务 2'));

实战要点:微任务在当前宏任务执行完毕后、浏览器准备进入渲染之前执行,因此它们的执行时机要比后续的宏任务更早,优先级更高。

1.3 宏任务与微任务的关系

在实际执行中,执行顺序通常遵循以下规律:先执行当前的 同步代码,再执行当前宏任务中的所有 微任务,接着进入下一轮宏任务的执行。若微任务在执行过程中又产生新的微任务,这些任务会继续被执行,直到微任务队列清空。

一个经典的示例能够直观展示两者的关系:

console.log('script start');
Promise.resolve().then(() => console.log('micro 1'));
setTimeout(() => console.log('macro 1'), 0);
Promise.resolve().then(() => console.log('micro 2'));
console.log('script end');

要点提示:执行顺序的核心在于“当前宏任务完成后清空微任务队列”,从而确保在进入下一轮宏任务前,所有相关的微任务都已经处理完毕。

2. 执行顺序全解析:从事件循环出发

2.1 事件循环与队列的基本结构

事件循环(Event Loop)是浏览器和 Node.js 用来调度异步任务的核心机制。它把异步工作分成两类队列:宏任务队列微任务队列,并在每次循环中协调执行顺序。

在每一轮循环中,系统会先执行所有当前宏任务,然后顺序执行该轮中的所有 微任务,最后才会考虑进行渲染(在浏览器环境下)。这个设计确保了不会因为异步回调导致渲染阶段错乱。

setTimeout(() => console.log('macro'), 0);
Promise.resolve().then(() => console.log('micro'));

要点提示:通过将微任务放在宏任务之中,事件循环能在每轮循环里优先处理尽可能多的微任务,确保状态更新的及时性。

2.2 任务执行的完整流程

标准的执行流程大致如下:同步代码先执行,随后进入当前宏任务,在它结束之前不会渲染。紧接着,事件循环会清空 微任务队列,再进入下一个宏任务。若微任务中又产生了新的微任务,它们会继续被执行,直到队列空。在渲染阶段,浏览器可能会在微任务清空后进行一次渲染。

如下代码演示了这一流程在不同环境中的预期行为(浏览器中常见):

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

要点提示:同步输出先于对异步操作的回调,微任务会在宏任务之间被执行,渲染往往在微任务清空后进行。

2.3 浏览器与 Node 的差异

浏览器环境除了宏任务与微任务外,还涉及渲染阶段,渲染会影响可见性更新时机。相比之下,Node 没有渲染阶段这一环节,微任务的优先级和执行时机也略有不同,process.nextTick 的优先级在某些场景下接近微任务但行为更高优先级。

为了跨环境的一致性,通常的做法是遵循“微任务在每个宏任务结束时清空”这一规则,并对 Node 与浏览器的差异进行额外处理。

// Node 环境示例:显示 nextTick 与 Promise 的顺序
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));

3. 实战要点:如何在代码中控制执行顺序

3.1 控制顺序的常用手段

在实际开发中,若需要让某些操作尽可能先于其他异步任务执行,应优先利用微任务来承载需要在当前轮次完成的回调。与此同时,应避免在微任务中执行耗时操作,以免阻塞后续的渲染。

一个常见的做法是将需要在当前轮次完成的异步工作放到 Promise.resolve().then 的回调中,或者使用 queueMicrotask 显式加入微任务队列。

async function run(){console.log('start');await new Promise(resolve => setTimeout(resolve, 0)); // 会引入一个宏任务console.log('after await (microtask runs before next macro task)');
}
run();

要点提示:通过把需要尽快完成、且不需要等待外部事件的工作放在微任务中,可以提高状态更新的响应性。

3.2 实战场景分析

场景一:在 DOM 更新后需要执行一个依赖新状态的计算,这时应将计算放在微任务中,以确保所有状态在渲染前就绪。场景二:需要定时轮询并在某个轮次结束后再执行回调,可以将定时触发放在宏任务中,以避免频繁的渲染中断。

console.log('S1');
Promise.resolve().then(() => console.log('Micro after S1'));
setTimeout(() => console.log('Macro after Micro'), 0);
console.log('S2');

要点提示:理解每种异步机制的触发时机,是进行性能优化与行为预测的关键。

4. 常见误区与坑点

4.1 误区一:setTimeout 就是微任务

许多开发者误以为 setTimeout 是一个微任务,但它实际上是一个 宏任务,会在当前轮次结束后进入下一轮循环执行。这个差异会直接影响执行顺序和渲染时间。

实际场景中,若你把 setTimeout 放在 Promise.then 的回调后,很多时候你会看到不同的输出顺序,因为微任务会先于宏任务执行。

console.log('A');
setTimeout(() => console.log('Macro'), 0);
Promise.resolve().then(() => console.log('Micro'));
console.log('B');

要点提示:避免把 setTimeout 当作微任务来理解,这有助于避免误判执行顺序。

4.2 误区二:微任务会在下一轮渲染前执行完毕

微任务是在当前宏任务结束后、进入下一轮事件循环前执行的,因此它们并非在下一次渲染之前就一定完成。若某些微任务需要进行大量计算,可能会阻塞渲染,导致性能下降。

正确的做法是:将密集计算分解成较小的微任务批次,或在合适的时点使用 requestAnimationFrame 来协调渲染与计算。

JavaScript 中的宏任务与微任务到底是什么?执行顺序全解析与实战要点

console.log('start');
Promise.resolve().then(() => {// 需要分步执行的大任务console.log('part one');
});
console.log('end');

要点提示:不要把所有工作都塞进一个微任务中,以免导致较长的微任务队列阻塞渲染。

4.3 误区三:所有异步回调都属于同一个执行阶段

不同的异步 API 的回调可能涉及不同的执行阶段。Promise 的回调通常进入 微任务队列,而 setTimeoutsetInterval 等属于 宏任务 队列。理解这一点能避免在复杂的异步场景中错把执行顺序混淆。

function a(){ console.log('a'); }
setTimeout(a, 0);
Promise.resolve().then(() => console.log('b'));
console.log('c');

要点提示:区分不同异步 API 的执行阶段,是编写可预测异步逻辑的基础。

广告