导读:本期聚焦于王柏年创作的《什么是JavaScript事件循环_宏任务与微任务执行顺序如何》,敬请观看详情。你写的setTimeout回调为什么总比Promise.then晚执行?这背后是JavaScript事件循环机制在起作用。事件循环负责协调同步代码、宏任务和微任务的执行次序,理解它才能掌握异步编程的节奏。本文从调用栈和任务队列的关系入手,解释事件循环的每一轮迭代如何处理宏任务与微任务,包括定时器延迟的真相、Promise与async/await在微任务中的表现、以及浏览器和Node.js事件循环的差异对比。通过代码示例与执行顺序分析,帮助你避开常见的异步执行顺序误判,写出更可靠高效的JavaScript代码。

JavaScript运行时是单线程的,意味着同一时间只能处理一个任务。但浏览器中又有网络请求、定时器、用户交互等并发场景,这些操作不可能都阻塞在主线程上等待完成。事件循环正是解决这个矛盾的协调机制:它维护一个调用栈和若干任务队列,决定什么任务可以进入主线程执行,以及按什么顺序执行。很多人以为异步回调就是按照代码书写顺序执行,实际情况远没有那么简单,宏任务与微任务的差异会让执行顺序出现反直觉的结果。

什么是JavaScript事件循环_宏任务与微任务执行顺序如何

事件循环到底在循环什么

事件循环可以理解成一个永不停歇的传感器,持续检查调用栈是否为空。调用栈存放当前正在执行的函数调用,当栈为空时,事件循环就会从任务队列中取出一个回调放进栈中执行。这个过程不断重复,就形成了循环。关键点在于,任务队列并不是单一的一条队伍,而是分为宏任务队列和微任务队列,二者的优先级完全不同。

宏任务包括script整体代码、setTimeout回调、setInterval回调、I/O操作回调、UI渲染回调等。微任务包括Promise.then回调、queueMicrotask、MutationObserver回调、以及async函数中await之后的代码。每个宏任务执行完毕,事件循环不会立刻去取下一个宏任务,而是先把当前微任务队列中的所有任务清空。这个规则是理解执行顺序的基石。

用一个直观的例子来验证这一点:

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
  console.log('promise1');
}).then(function () {
  console.log('promise2');
});

console.log('script end');

这段代码的输出顺序是script start、script end、promise1、promise2、setTimeout。同步代码最先跑完,接着所有微任务执行,最后才轮到宏任务。即使setTimeout的延迟设为0,也依然排在微任务之后。

宏任务与微任务的执行优先级

执行顺序的本质可以用一句话概括:每执行一个宏任务,都要先把该宏任务产生的新微任务以及队列中已有的微任务全部处理完,然后再取下一个宏任务。这个过程与同步代码的关系是,同步代码本身被视为第一个宏任务的一部分,所以同步代码执行完会紧接着执行它引发的微任务。

这里有个容易误判的场景,微任务执行过程中如果又产生了新的微任务,它也会被追加到当前微任务队列,并且继续执行直到队列清空。这意味着一个微任务递归地往队列里添加微任务,会导致无限循环,阻塞后续宏任务。例如下面这段代码会让页面卡死:

function loopMicrotask() {
  queueMicrotask(() => {
    console.log('still microtask');
    loopMicrotask();
  });
}

loopMicrotask();

console会无限输出,浏览器会失去响应。因此在实际编码中,微任务中适合执行轻量级操作,避免大量递归。而宏任务因为每次都要重新从任务队列取任务,天然具有让出主线程的机会,更适合处理耗时较长的分段任务。

宏任务和微任务的这种协作方式也决定了事件循环的每一轮“tick”包含哪些步骤:执行宏任务、清理微任务、决定是否渲染更新。渲染更新在微任务清理之后进行,因此在一个宏任务内多次修改DOM并不会触发多次渲染,浏览器会把渲染合并到微任务清空之后。

Promise与async/await的执行顺序

Promise构造函数中的执行器函数是同步执行的,只有其中的resolve或reject调用才产生微任务。所以new Promise时会立即执行执行器里的代码,而.then注册的回调会进入微任务队列。async函数则相当于在函数内部自动包装了Promise,遇到await时会暂停函数执行,await后面的代码会被放入微任务队列继续运行。

来看一个async和普通Promise混合的场景:

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

async1();

new Promise(function (resolve) {
  console.log('promise1');
  resolve();
}).then(function () {
  console.log('promise2');
});

console.log('script end');

输出结果是script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout。这里细节非常关键:async2执行完毕返回Promise,async1中的await会暂缓后续代码,而后续代码的优先级等同于Promise.then,所以async1 end和promise2都作为微任务执行。由于async1 end的微任务先被注册,因此它先于promise2输出。

在面试和实际排错中,这种顺序经常被拿来考察对事件循环的理解。用工具验证执行顺序时,可以在浏览器控制台运行,也可以使用Node.js的环境,但需要注意Node.js的微任务和宏任务模型与浏览器存在某些差异,特别是旧版Node.js对不同阶段任务的处理方式不同。

事件循环中的任务队列还有什么细节

浏览器将宏任务队列又细分成多个来源,比如定时器任务、用户交互任务、网络任务等,但规范并未规定不同来源之间的严格顺序,各浏览器可能略有差异。定时器回调虽然属于宏任务,但setTimeout的延迟计时是独立线程处理的,到时间后回调才会被放入任务队列。所以setTimeout的延迟并不精确,它只保证最早在延迟时间后执行,实际执行时间取决于主线程是否空闲以及前序任务是否耗尽。

requestAnimationFrame也处于事件循环中,但它与普通任务队列不同,它更贴近渲染步骤。如果在一个宏任务中同时有setTimeout回调与requestAnimationFrame回调,rAF通常会先于渲染执行,而且rAF会被合并到同一帧内。这个特性适合动画更新,但不适合作为精确的定时器。

Node.js的事件循环分成多个阶段:timers、pending callbacks、idle/prepare、poll、check、close callbacks。每个阶段都会执行该阶段自己的宏任务,然后进入process.nextTick与微任务队列。process.nextTick的运行时机比Promise.then更早,这一点与浏览器环境差异明显,需要单独记忆。如果浏览器项目里运行相同的代码,nextTick并不存在,因此不要把Node特定的API混入浏览器代码中。

面对复杂嵌套的异步任务,可以利用浏览器开发者工具或Node环境动态观察执行顺序。例如在Chrome的Performance面板中录制一段运行脚本,能够清晰看到任务被分派和执行的耗时分布。调试事件循环问题时,手动添加日志输出是最直接的验证方式,但要注意console本身也可能产生异步行为,不过绝大多数情况下不影响结果判断。

常见误区与实战建议

一个常见误区是认为setTimeout延迟为0就是指立即执行。实际上0只是一个最小延迟值,浏览器为了保证页面性能,通常会设置一个最小钳制值,比如嵌套超过一定层数后最小延迟变为4毫秒。如果把大量任务通过setTimeout(0)推进队列,它们依然需要等待主线程空闲以及微任务清空后才能运行。

另一个误区是把Promise.all的调用顺序与回调顺序混淆。Promise.all接收的数组中的Promise是同步创建的,但状态变更后的回调会按照注册顺序进入微任务队列,因此回调输出顺序与传入的Promise顺序一致。这与for循环中发起异步请求的乱序回信息是不同的,后者由网络I/O完成时间决定。

在实战中对顺序有严格要求时,应当优先使用微任务来串联依赖步骤,因为微任务在同一宏任务内执行,不会导致中间插入意外的其他宏任务。例如要在一个函数中先更新缓存,再根据缓存计算渲染数据,利用Promise.then可以保证缓存更新在下一次渲染前完成。而涉及动画、用户输入响应或大数据块处理时,适合用setTimeout或requestAnimationFrame把任务拆分成宏任务,避免长时间占用主线程。

最后,阅读和理解事件循环的核心在于抓住一个模型:调用栈负责同步执行,微任务队列在每次宏任务结束后清空,宏任务队列在微任务清空后继续推进。掌握了这个模型,再配合实际代码进行调试验证,JavaScript的异步执行顺序就不再神秘了。代码中需要避免破坏这个模型的行为,比如在微任务中无限循环、依赖不确定的宏任务顺序,以及把CPU密集型任务放在微任务中。合理利用宏任务与微任务的特性,才能让异步代码既高性能又可维护。

JavaScript事件循环宏任务微任务修改时间:2026-08-28 23:10:22

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。