深入解析JavaScript事件循环:宏任务与微任务的执行顺序

作者:微光

时间:2026-10-23

浏览:604

本文深入解析JavaScript单线程模型下的事件循环机制,区分宏任务与微任务的执行优先级。通过剖析setTimeout、Promise及process.nextTick的运行逻辑,阐明主线程、任务队列与微任务队列的交互流程,帮助开发者掌握异步代码的实际执行顺序。

文章目录

JavaScript单线程与事件循环概述

事件循环(Event Loop)是JavaScript开发者必须掌握的核心概念,初学者往往对此存在诸多疑问。

JavaScript采用单线程模型,意味着同一时刻只能执行一个任务。虽然这通常不会造成问题,但当遇到耗时较长的任务时,必须等待其执行完毕才能处理后续任务。这种阻塞会导致页面假死,用户在此期间无法进行任何交互,体验极差。

为解决单线程的局限性,浏览器提供了JavaScript引擎之外的Web API功能,包括DOM操作、定时器及HTTP请求等。这些API支持异步非阻塞行为,而事件循环机制正是协调这些异步任务的关键所在。

为何JavaScript被设计为单线程?

多线程允许程序同时独立执行多个部分,判断语言是否单线程最简单的方法是查看调用栈的数量。JavaScript仅有一个调用栈,因此属于单线程语言。

JavaScript设计为单线程是由其运行环境和用途决定的。作为浏览器脚本,JS主要承担与用户交互及操作DOM的职责。若采用多线程,将引发复杂的同步问题。此外,单线程执行效率更高,符合脚本语言的需求。

传统事件循环机制回顾

1. 对事件循环的旧有认知

传统观点认为,主进程执行完毕后,会从任务队列中依次取出任务执行。如图所示,任务分为同步和异步两类:同步任务直接进入主执行栈;异步任务则先挂起,待有返回值后进入任务队列,再由主程序执行。异步任务通过先进先出的队列机制进行协调。

JavaScript单线程与事件循环概述

同步与异步任务进入不同的执行环境:同步任务进入主线程(主执行栈),异步任务进入任务队列。当主线程任务执行完毕为空时,会从任务队列读取任务推入主线程执行。这一重复过程即事件循环。然而,随着Promise的出现,这种简化说法已不够准确。

宏任务与微任务的执行机制

2. 对事件循环的新认知

2.1 理论基础

首先通过一张图展示JavaScript事件循环的整体流程:

JavaScript单线程与事件循环概述

初看此图,读者可能会感到困惑。

这里将任务分为两大类:macroTask(宏任务)和microTask(微任务)。一次事件循环的执行顺序为:先运行macroTask队列中的一个任务,然后执行microTask队列中的所有任务。随后开始下一次循环(仅针对这两类任务,完整的事件循环更为复杂)。

那么,什么是macroTask?什么是microTask?

JavaScript引擎将所有任务分类,一部分归为macroTask,另一部分归为microTask。具体分类如下:

macroTask包括:

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI rendering

microTask包括:

  • process.nextTick
  • Promise
  • Object.observe
  • MutationObserver

常见的定时器属于macroTask,但仅了解macroTask机制是不够的。为了直观感受两种队列的区别,下面通过代码进行实践。

代码实践与执行顺序分析

2.2 代码实践

以setTimeout、process.nextTick和Promise为例,直观展示两种任务队列的运行方式。


console.log('main1');

process.nextTick(function() {
 console.log('process.nextTick1');
});

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

new Promise(function(resolve, reject) {
 console.log('promise');
 resolve();
}).then(function() {
 console.log('promise then');
});

console.log('main2');

在查看结果前,请先根据上述理论推测运行结果。

实际输出结果如下:

main1 promise main2 process.nextTick1 promise then

// 第二次事件循环 setTimeout process.nextTick2

process.nextTick和promise then在setTimeout之前输出,证明了macroTask和microTask的执行顺序。但需注意,上图易产生误解,让人以为主进程代码执行后先调用macroTask再调用microTask,导致第一个循环中macroTask在前。

实践表明,在第一个循环中,process.nextTick1和promise then这两个microTask在setTimeout这个macroTask之前输出。这是因为Promises/A+规范规定主进程代码也属于macroTask。

主进程这个macroTask(包含main1、promise和main2)执行完毕后,自然会去执行process.nextTick1和promise then这两个microTask,这是第一个循环。随后的setTimeout和process.nextTick2属于第二个循环。

上述代码看似复杂,只要理清原理,逻辑便清晰明了。

requestAnimationFrame、Object.observe(已废弃)和MutationObserver的运行机制类似,区别仅在于具体用法。重点在于UI rendering。根据HTML规范event-loop-processing-model,在处理完macroTask和microTask后,会执行Update the rendering,即进行一次UI重新渲染。

事件循环总结

3. 总结

综上所述,一次事件循环的执行顺序为:先运行macroTask队列中的一个任务,然后执行microTask队列中的所有任务,最后开始下一次循环。

参考文献:

  • JavaScript Event Loop相关原理解析
  • 深入理解事件循环机制
  • JavaScript运行机制

以上就是深入分析JavaScript事件循环(Event Loop)的详细内容,更多关于JavaScript事件循环(Event Loop)的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章