JavaScript 宏任务与微任务及 Event Loop 机制详解

作者:纸飞机

时间:2026-10-26

浏览:248

本文详解 JavaScript 单线程模型下的 Event Loop 机制,区分宏任务与微任务的执行优先级。通过同步代码、Promise、setTimeout 等示例,解析控制台输出顺序,阐明异步任务在事件循环中的调度逻辑及浏览器与 Node.js 环境的差异。

文章目录

前言

首先需要明确,JavaScript 是一门单线程脚本语言,这意味着在代码执行过程中,同一时刻只能处理一个进程(即执行一段代码),无法并行运行多个进程。

JavaScript 将任务执行模式划分为两种:同步(Synchronous)和异步(Asynchronous)。

同步模式指当一个进程发起请求后,若该请求耗时较长,进程会一直等待直到收到响应信息,随后才继续执行后续代码。

异步模式则允许进程无需等待,直接继续执行后续操作,忽略其他进程的状态。当有消息返回时,系统会通知进程进行处理,从而显著提升执行效率。

进程狭义上指正在运行的程序实例;广义上指具有一定独立功能的程序关于某个数据集合的一次运行活动,主要描述 CPU 运行指令及加载、保存上下文所需的时间。

线程是程序中单一的顺序控制流程,是进程内相对独立且可调度的执行单元,也是系统独立调度和分派 CPU 的基本单位,即运行中程序的调度单位。

执行栈是 V8(谷歌浏览器引擎)内部维护的一种栈结构,用于存放函数的执行上下文环境。

许多人可能存有疑惑:既然 JavaScript 是单线程脚本语言,其优势何在?为何不采用多进程设计?

  • 单线程设计有助于节省内存资源,同时减少上下文切换的时间开销;
  • 此外,单线程可避免与渲染线程发生冲突。若 JavaScript 采用双线程,当页面渲染线程仍在执行时,JS 线程若修改了渲染页面的参数,将导致页面渲染出现异常。

宏任务

(macro)task 可理解为每次执行栈执行的代码即为一个宏任务(包括每次从事件队列中获取一个事件回调并放入执行栈中执行)。

宏任务队列

  • I/O
  • UI-rendering(页面渲染)
  • script
  • setTimeout
  • setInterval
  • setImmediate(在 Node 环境下属于宏任务,而在浏览器环境中不属于)
  • requestAnimationFrame(在浏览器环境中属于宏任务,而在 Node 环境中不属于)

根据 MDN 定义,requestAnimationFrame 指下次页面重绘前所执行的操作。重绘作为宏任务的一个步骤存在,且该步骤晚于微任务的执行。

微任务

microtask 可理解在当前 task 执行结束后立即执行的任务。即在当前 task 任务完成后、下一个 task 开始前,且在页面渲染之前执行。

因此,微任务的响应速度相比 setTimeout(属于 task)更快,因为无需等待渲染。具体而言,在某个 macrotask 执行完毕后,会立即执行其执行期间产生的所有 microtask(在渲染前完成)。

微任务队列

  • process.nextTick(在 Node 环境下属于微任务,而在浏览器环境中不属于)
  • promise.then
  • MutationObserver(在浏览器环境中属于微任务,而在 Node 环境中不属于)

Event-Loop

前文已详述宏任务、微任务等各类任务的执行方式,那么 Event Loop 究竟为何物?

JavaScript 是单进程语言,同一时间无法处理多个任务。因此,需要 Event Loop(事件循环机制,计算机系统的一种运作机制)这一判断逻辑来决定何时执行宏任务、何时执行微任务。

同步代码:JavaScript 作为脚本语言,对同步代码采取自上而下的解释执行方式。

异步代码:一个任务被分为多个片段执行,先执行一段;若遇到耗时操作(如本地或网络 IO 请求),第一段开始 IO 后,执行权将移交给其他任务;当 IO 完成后,再执行后半段任务(由于 IO 操作耗时较长,系统在 IO 期间主线程保持空闲)。

执行顺序

1. 首先执行同步代码,这属于宏任务;

2. 当所有同步代码执行完毕后,执行栈为空,此时检查是否有异步代码待执行;

3. 执行微任务;

4. 微任务执行完毕后,如有必要则渲染页面;

5. 开启下一轮 Event Loop,执行宏任务中的代码;

代码在执行过程中,若遇到异步代码,会将该异步代码装入队列(挂起)。

实战出真理,我们用一个🌰感受一下
console.log('start');          // 1
function foo(){
   console.log('foo');           
}
foo()                          // 2
setTimeout(function() {        //异步代码中的宏任务,先挂起
   console.log('setTimeout'); // 7
},1000)
new Promise(resolve =>{
   console.log('promise');    // 3
   resolve()
})
   .then(function() {        //异步代码中的微任务,先挂起
       console.log('promise1');// 5
   })
   .then(function() {        //异步代码中的微任务,先挂起
       console.log('promise2');// 6
   })
console.log('end');            // 4

我们跟随执行顺序进行分析:第一步首先执行同步代码,因此依次打印出 'start',调用函数 foo 打印出 'foo',执行 Promise 构造函数打印出 'promise',最后打印出 'end',此时同步代码执行完毕。接着检查是否有异步代码待执行,第三步执行微任务,因此先打印出第一微任务列表中的 'promise1',紧接着打印第二微任务列表中的 'promise2'。由于纯 JS 无需渲染页面,随后开启下一轮 Event Loop 执行宏任务代码。而 setTimeout 属于异步代码中的宏任务,因此最终打印出 'setTimeout'。最后,我们查看在谷歌浏览器中的实际打印效果。

前言

这里有一个小细节:若将定时器 setTimeout 的时间设置为 0,结果依然相同。setTimeout 的执行顺序不会因时间设置而改变,因为它始终属于异步代码中的宏任务,不会因延迟执行的时间长短而改变优先级。

结语

以上是我对宏任务、微任务以及事件循环机制(Event Loop)的一些浅显认识与理解,更多关于 JS 宏任务、微任务及 Event Loop 的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章