JavaScript事件循环之宏任务与微任务的执行机制深入理解

 更新时间:2025年12月21日 10:39:19   作者:fruge365  
JavaScript的事件循环机制是理解异步编程和性能优化的关键,下面这篇文章主要介绍了JavaScript事件循环之宏任务与微任务执行机制的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

JavaScript 是单线程语言,但它却能处理复杂的并发操作(如网络请求、定时器、用户交互),这背后的秘密武器就是 事件循环(Event Loop)。本文将深入拆解宏任务与微任务的执行逻辑,通过代码示例帮你彻底搞懂执行顺序。

TL;DR

  • 核心机制:JS 引擎执行同步代码 -> 清空微任务队列 -> 尝试 DOM 渲染 -> 执行一个宏任务 -> 清空微任务队列 -> … 循环往复。
  • 微任务(MicroTask):优先级高,在当前宏任务结束后立即执行。包括 Promise.thenprocess.nextTick (Node)、MutationObserver
  • 宏任务(MacroTask):优先级低,每次循环只执行一个。包括 setTimeoutsetIntervalsetImmediate (Node)、I/O、UI Rendering。
  • 关键点:微任务队列总是会在下一个宏任务开始之前被清空。

1. 为什么需要事件循环?

JavaScript 的设计初衷是作为浏览器脚本语言,主要用途是与用户互动和操作 DOM。如果它是多线程的,一个线程在删除 DOM 节点,另一个线程在编辑该节点,会带来复杂的同步问题。因此,JS 选择 单线程 执行。

为了不阻塞主线程(例如等待一个 5秒的 API 请求),JS 引入了 异步非阻塞 机制,而事件循环正是协调同步代码与异步回调执行顺序的调度员。

2. 宏任务与微任务的分类

并不是所有的异步任务都是一样的。它们被分为两类队列:

微任务 (MicroTask)

通常是由代码本身产生的任务,优先级较高,需要在当前同步代码执行完后立即处理。

  • Promise.then / .catch / .finally
  • process.nextTick (Node.js 环境,优先级高于 Promise)
  • MutationObserver (监听 DOM 变化)
  • queueMicrotask API

宏任务 (MacroTask)

通常是由宿主环境(浏览器或 Node)发起的任务,每次事件循环只取一个执行。

  • setTimeout / setInterval
  • setImmediate (Node.js)
  • requestAnimationFrame (UI 渲染前执行,归类有些特殊,通常视为渲染阶段的一部分)
  • I/O 操作 (文件读写、网络请求回调)
  • UI Rendering (浏览器绘制)
  • <script> (整体代码本身算第一个宏任务)

3. 事件循环的完整流程

标准的 Event Loop 流程如下:

  1. 执行同步代码(这本身属于第一个宏任务)。
  2. 检查微任务队列
    • 如果队列不为空,取出队首任务执行。
    • 执行过程中如果产生了新的微任务,追加到队尾,继续执行直到队列清空
  3. UI 渲染阶段(浏览器视情况决定是否渲染):
    • 检查是否需要更新 UI。
    • 执行 requestAnimationFrame 回调(如果在渲染前)。
  4. 执行宏任务
    • 从宏任务队列中取出一个任务执行。
    • 执行完后,回到第 2 步(再次清空微任务)。

口诀:同步走完清微任务,渲染之后取宏任务。

4. 实战代码解析

案例一:基础顺序

console.log('1'); // 同步

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

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

console.log('4'); // 同步

解析

  1. 执行同步代码:打印 '1',打印 '4'
  2. 清空微任务:执行 Promise 回调,打印 '3'
  3. 执行宏任务:执行 setTimeout 回调,打印 '2'
    结果1 -> 4 -> 3 -> 2

案例二:微任务插队与嵌套

console.log('Start');

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

Promise.resolve().then(() => {
  console.log('Promise 1'); // 微任务 1
  // 微任务中产生新的微任务
  Promise.resolve().then(() => {
    console.log('Promise 2'); // 微任务 2
  });
});

console.log('End');

解析

  1. 同步打印 'Start', 'End'
  2. 检查微任务队列:发现 Promise 1,执行并打印。
  3. Promise 1 执行时注册了 Promise 2,追加到当前微任务队列尾部。
  4. 微任务队列未空,继续执行 Promise 2,打印。
  5. 微任务清空完毕,去宏任务队列取 Timeout 执行。
    结果Start -> End -> Promise 1 -> Promise 2 -> Timeout

案例三:async/await 的本质

async/await 只是 Promise 的语法糖。await 这一行右边的代码是同步执行的,await 下面的代码 相当于放在了 Promise.then 中,属于微任务。

async function async1() {
  console.log('async1 start');
  await async2(); 
  // 下面这行相当于 .then(() => console.log('async1 end'))
  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'); // Promise 构造函数内是同步的
  resolve();
}).then(function() {
  console.log('promise2');
});

console.log('script end');

深度解析

  1. script start (同步)
  2. setTimeout 注册宏任务。
  3. 调用 async1:
    • 打印 async1 start (同步)。
    • 调用 async2,打印 async2 (同步)。
    • 遇到 await,将 async1 end 放入微任务队列 (微任务1)。
  4. new Promise:
    • 打印 promise1 (同步)。
    • resolve() 触发 then,将 promise2 放入微任务队列 (微任务2)。
  5. 打印 script end (同步)。
  6. 同步结束,清空微任务
    • 执行微任务1:打印 async1 end
    • 执行微任务2:打印 promise2
  7. 微任务空,执行宏任务
    • 打印 setTimeout

结果
script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout

(注:旧版 Chrome 曾有 Bug 导致 async1 end 比 promise2 慢,但在现代浏览器中已符合标准,遵循入队顺序)

5. 易错点与注意事项

  1. Promise 构造函数是同步的new Promise(fn) 中的 fn 会立即执行,只有 .then 中的回调才是微任务。
  2. 微任务饿死宏任务:如果你在微任务中无限循环地添加新的微任务(例如递归 Promise),那么主线程会一直被占用,宏任务永远无法执行,页面会卡死(类似 while(true))。
  3. UI 渲染时机:通常浏览器会在清空微任务之后、执行下一个宏任务之前尝试渲染。如果微任务执行时间过长,会阻塞渲染导致掉帧。
  4. Node.js 的差异
    • process.nextTick 优先级高于 Promise。
    • 早期的 Node (v10及以前) 在执行完一个阶段的所有宏任务后才清空微任务,但 Node v11+ 已修改为与浏览器一致:每执行完一个宏任务就清空一次微任务

总结

掌握事件循环的关键在于分清 同步代码微任务宏任务 的层级。始终记住:微任务是 VIP 通道,必须优先走完;宏任务是普通通道,一次只能走一个。

到此这篇关于JavaScript事件循环之宏任务与微任务执行机制的文章就介绍到这了,更多相关JS宏任务与微任务执行机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JavaScript实现消消乐的源代码

    JavaScript实现消消乐的源代码

    这篇文章主要介绍了JavaScript实现消消乐-源代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • 自定义PC微信扫码登录样式写法

    自定义PC微信扫码登录样式写法

    这篇文章主要介绍了自定义PC微信扫码登录样式的写法以及做了代码分析,需要的朋友学习下吧。
    2017-12-12
  • JavaScript使用小插件实现倒计时的方法讲解

    JavaScript使用小插件实现倒计时的方法讲解

    今天小编就为大家分享一篇关于JavaScript使用小插件实现倒计时的方法讲解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-03-03
  • 基于JavaScript操作DOM常用的API小结

    基于JavaScript操作DOM常用的API小结

    DOM(Document Object Model)即文档对象模型,针对 HTML 和 XML 文档的 API(应用程序接口)。本篇文章给大家介绍javascript操作dom常用的api小结,对javascript dom api相关知识感兴趣的朋友一起学习吧
    2015-12-12
  • Js base64 加密解密介绍

    Js base64 加密解密介绍

    想必大家对base64并不陌生吧,在本文将为大家介绍下Js中的base64加密解密过程,感兴趣的朋友不要错过
    2013-10-10
  • JavaScript 空位补零实现代码

    JavaScript 空位补零实现代码

    JavaScript代码实现空位补零
    2010-02-02
  • 使用JS画图之点、线、面

    使用JS画图之点、线、面

    这篇文章主要介绍了使用js绘制几何图形的基础,绘制点、线、面,需要的朋友可以参考下
    2015-01-01
  • javascript实现原生ajax的几种方法介绍

    javascript实现原生ajax的几种方法介绍

    项目中不需要加载jquery这种庞大的js插件要使用到ajax这种功能该如何办呢?下面和大家分享几种利用javascript实现原生ajax的方法
    2013-09-09
  • el-upload实现上传文件并展示进度条功能

    el-upload实现上传文件并展示进度条功能

    这篇文章主要介绍了el-upload实现上传文件并展示进度条,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • javascript实现鼠标拖动改变层大小的方法

    javascript实现鼠标拖动改变层大小的方法

    这篇文章主要介绍了javascript实现鼠标拖动改变层大小的方法,涉及javascript操作鼠标事件及样式的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-04-04

最新评论