news 2026/2/4 2:22:27

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解 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 变化)
  • queueMicrotaskAPI

宏任务 (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中,属于微任务。

asyncfunctionasync1(){console.log('async1 start');awaitasync2();// 下面这行相当于 .then(() => console.log('async1 end'))console.log('async1 end');}asyncfunctionasync2(){console.log('async2');}console.log('script start');setTimeout(function(){console.log('setTimeout');},0);async1();newPromise(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 通道,必须优先走完;宏任务是普通通道,一次只能走一个。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/2 13:21:28

Sanic框架CLI参数解析异常深度解析与修复指南

Sanic框架CLI参数解析异常深度解析与修复指南 【免费下载链接】sanic Accelerate your web app development | Build fast. Run fast. 项目地址: https://gitcode.com/gh_mirrors/sa/sanic Sanic作为Python生态中备受瞩目的异步Web框架&#xff0c;以其卓越的性能表现和…

作者头像 李华
网站建设 2026/1/29 16:29:27

效率对比:传统vs AI辅助开发Vue-Baidu-Map

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 请生成两份对比代码&#xff1a;1.传统方式手动编写的vue-baidu-map基础功能实现&#xff1b;2.使用AI生成的相同功能代码。功能包括&#xff1a;地图初始化、标记点添加、信息窗口…

作者头像 李华
网站建设 2026/2/2 18:38:01

Bodymovin/Lottie完整教程:从零开始掌握动画导出与渲染

Bodymovin/Lottie完整教程&#xff1a;从零开始掌握动画导出与渲染 【免费下载链接】lottie-web 项目地址: https://gitcode.com/gh_mirrors/lot/lottie-web 还在为精美的After Effects动画无法直接应用到网页而烦恼&#xff1f;设计师与开发者的工作流程脱节导致项目延…

作者头像 李华
网站建设 2026/1/31 14:08:26

Figma汉化插件实战:跨国团队协作的救星

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个详细的Figma汉化插件使用教程项目&#xff0c;包含逐步安装指南、配置说明和常见问题解答。要求演示如何在不同场景下使用插件&#xff0c;如UI设计审查、团队协作和客户演…

作者头像 李华
网站建设 2026/1/31 0:13:46

永磁同步电机控制玩的就是环套环的把戏,今天咱们拆解一套RSMDO+DBCC组合拳。这玩意儿在工业现场对付参数变化和负载扰动特别带劲,不信你看完下面的代码实操

永磁同步电机传统滑模扰动观测器控制&#xff08;RSMDO&#xff09;&#xff0b;无差电流预测控制&#xff08;DBCC&#xff09; [1]速度环采用RSMDO [2]电流环采用DBCC 本系列仿真所使用的电机参数一致。速度环交给RSMDO算是找对人了&#xff0c;这哥们对付转速波动就跟猫抓老…

作者头像 李华
网站建设 2026/2/3 0:53:09

2024年CUPT技术探索:水滴透镜comsol仿真技术与实际应用分析

2024CUPT水滴透镜comsol仿真最近在研究2024CUPT的水滴透镜项目&#xff0c;发现用COMSOL进行仿真真的挺有意思的。今天就聊聊怎么用COMSOL来模拟水滴透镜的光学行为&#xff0c;顺便插点代码&#xff0c;看看能不能让这个过程更清晰一点。首先&#xff0c;水滴透镜的基本原理就…

作者头像 李华