深入理解JavaScript事件循环Event Loop:宏任务与微任务的奇幻之旅

2024-05-11 18:52

本文主要是介绍深入理解JavaScript事件循环Event Loop:宏任务与微任务的奇幻之旅,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

🔥 个人主页:空白诗

在这里插入图片描述

文章目录

  • 🎉 引言
    • 🌟 什么是事件循环?
    • 📚 「宏任务」 vs 「微任务」
      • 「宏任务」(Macrotask)
      • 「微任务」(Microtask)
      • 实际应用中的注意事项
    • 🔀 执行流程概览
    • 📝 代码示例
    • 🔍 深入理解「微任务」执行时机
      • 「微任务」的执行流程特点
      • 示例代码加深理解:
    • 💡 实践建议
      • 1. 避免过度依赖「微任务」
      • 2. 控制异步逻辑复杂度
      • 3. 注意异步操作对UI更新的影响
      • 4. 性能监控与调试
    • 🎖 总结
    • 🔗 相关链接

🎉 引言

JavaScript的世界里,一切皆为异步编程的魔法所驱动,而事件循环(Event Loop)正是这魔法背后的神秘引擎🔍。它确保了我们的代码能够高效、有序地执行,即使面对复杂的异步操作也能游刃有余。本文将带你深入探索这一机制,通过理论结合实践,揭开「宏任务」(Macrotasks)与「微任务」(Microtasks)的神秘面纱✨。


🌟 什么是事件循环?

事件循环是JavaScript运行时环境(如浏览器或Node.js)中用于协调执行代码的一种机制。它负责管理和执行两种类型的任务队列:「宏任务」(Macrotasks)和「微任务」(Microtasks),以确保异步操作能够有序且高效地完成,同时保证了代码的非阻塞执行特性。

在这里插入图片描述

具体来说,事件循环的工作流程遵循以下步骤:

  1. 执行全局脚本代码:当JavaScript引擎开始执行一段脚本时,它首先会执行所有的同步代码,这些代码被视为当前「宏任务」的一部分。

  2. 检查「微任务」队列:在当前「宏任务」执行完毕后,事件循环会查看「微任务」队列。如果有待处理的「微任务」,事件循环会清空整个「微任务」队列,即连续执行队列中的所有「微任务」,直到队列为空。这一过程会在每次「宏任务」结束后重复,确保「微任务」能够迅速得到响应。

  3. 执行宏任务:在处理完当前所有「微任务」后,事件循环会从「宏任务」队列中取出下一个任务并执行。这包括像setTimeoutsetInterval的回调、I/O操作完成、UI渲染等。

  4. 重复循环:一旦当前「宏任务」执行完毕,事件循环会再次检查是否有新的「微任务」需要处理,如此反复循环,直至「宏任务」队列和「微任务」队列均为空,或者有外部中断(如用户交互)发生。

事件循环的关键在于它确保了异步操作能够插入到同步代码执行的间隙中,既不会阻碍同步代码的执行,又能及时响应各种事件和数据变化,是实现JavaScript异步编程模型的基础。


📚 「宏任务」 vs 「微任务」

JavaScript的异步编程模型中,任务被划分为两大类:宏任务(Macrotasks)微任务(Microtasks)。这两者在事件循环中的处理方式和执行时机有着本质的不同,深刻理解它们对于编写高性能和可预测性的异步代码至关重要。

「宏任务」(Macrotask)

定义与特点:

  • 「宏任务」代表了执行环境中较重的异步操作,它们由宿主环境(如浏览器或Node.js)直接控制。
  • 这些任务通常涉及I/O操作、用户交互(如点击事件)、UI渲染以及基于定时器的功能(setTimeoutsetInterval)。
  • 每次事件循环的迭代中,最多只会执行一个「宏任务」 ,执行完毕后,事件循环会检查是否存在待处理的「微任务」

常见例子:

  • DOM事件处理(如点击、加载事件)
  • setTimeoutsetInterval 的回调
  • I/O 操作(文件读写)
  • UI渲染

「微任务」(Microtask)

定义与特点:

  • 相比之下,「微任务」更为轻量级,它们通常由JavaScript引擎自身管理,用于执行一些需要快速响应的操作。
  • 「微任务」在当前「宏任务」 结束后立即执行,且在下一个「宏任务」 开始前必须全部执行完毕,这使得「微任务」具有更高的优先级。
  • 在一个「宏任务」 执行期间,可以生成多个「微任务」,这些「宏任务」会在当前「宏任务」结束后形成一个队列,并一次性执行完毕。

常见例子:

  • Promise 的回调(.then.catch.finally
  • MutationObserver 的回调
  • queueMicrotask(在某些环境中提供,作为「微任务」的直接API)
  • Node.js 中的 process.nextTick

实际应用中的注意事项

  • 性能考量:由于微任务在同一个「宏任务」内连续执行,应避免在「微任务」中执行大量计算或产生大量新的「微任务」,以防调用栈过深导致性能问题。
  • 任务调度:合理安排「宏任务」「微任务」的使用,以控制代码执行的时机和顺序,特别是在处理异步逻辑时。

理解「宏任务」「微任务」的执行模型,可以帮助开发者更好地设计异步流程,避免因执行时机不当引发的bug,提升应用的响应速度和用户体验。


🔀 执行流程概览

  1. 全局脚本开始执行:当JavaScript引擎开始运行时,它会先执行全局脚本的同步代码部分。这是每个事件循环的起点,包括变量声明、函数定义等。。
  2. 执行当前「宏任务」:在执行过程中遇到诸如setTimeoutsetIntervalI/O请求或用户交互事件(在它们实际触发时),它们的处理逻辑会被安排到未来的「宏任务」队列中,而不是立即执行。同时,如果遇到新创建的Promise并且其内部有.then.catch,这些回调会被加入「微任务」队列
  3. 执行所有可执行的「微任务」:在当前「宏任务」执行完毕后,会检查「微任务」队列(这包括了Promise回调、MutationObserver回调等),「微任务」队列中的所有任务会在这个阶段被连续执行,直到队列为空,然后再继续下一轮事件循环。
  4. 渲染页面(如果有必要):在所有「微任务」执行完成后,浏览器判断是否需要重新渲染页面。如果DOM结构、CSS样式等有变化,此时可能触发UI更新。。
  5. 检查新的「宏任务」:一旦「微任务」队列 清空且完成可能的渲染工作,事件循环会检查是否有新的「宏任务」到达(例如来自网络的响应、定时器到期),并开始执行下一个「宏任务」的同步代码部分。整个过程形成了一个循环,不断迭代,直到「宏任务」队列为空且没有新的任务加入,此时JavaScript运行环境可能会进入休眠状态,等待新的事件触发。

在这里插入图片描述


📝 代码示例

下面的代码将帮助你直观理解「宏任务」「微任务」的执行顺序:

console.log('Start'); // 1. 同步代码首先执行setTimeout(() => {console.log('setTimeout'); // 4. 宏任务,最后执行
}, 0);new Promise((resolve) => {console.log('Promise'); // 2. 同步代码块内的Promise立即执行resolve();
}).then(() => {console.log('Promise.then'); // 3. 微任务,在当前宏任务的末尾执行
});// 输出顺序: Start -> Promise -> Promise.then -> setTimeout

这段代码展示了JavaScript「宏任务」(Macrotask)与「微任务」(Microtask)的执行顺序。下面是详细的解释,配合代码注释:

console.log('Start'); // 1. 同步代码首先执行
  • 同步代码执行:首先,JavaScript会执行全局脚本中的同步代码,输出"Start"。
setTimeout(() => {console.log('setTimeout'); // 4. 宏任务,最后执行
}, 0);
  • setTimeout 宏任务调度:接着,遇到setTimeout函数,它安排了一个「宏任务」到任务队列。虽然延迟时间设为0,但这不代表立即执行,而是意味着至少在当前脚本执行完成后,且最早在下一次事件循环开始时执行。因此,"setTimeout"的输出会晚于「微任务」
new Promise((resolve) => {console.log('Promise'); // 2. 同步代码块内的Promise立即执行resolve();
}).then(() => {console.log('Promise.then'); // 3. 微任务,在当前宏任务的末尾执行
});
  • Promise构造函数执行:创建并立即执行Promise构造函数中的代码,输出"Promise"。这是同步执行的一部分。

  • Promise.then 微任务调度:当Promise被解析(这里通过resolve()方法),其关联的.then方法中的回调会被加入到「微任务」队列。因为「微任务」总是在当前「宏任务」的最后执行,所以在"Promise"之后,紧接着输出"Promise.then"。

总结输出顺序: Start -> Promise -> Promise.then -> setTimeout

这清晰地表明了JavaScript运行时的事件循环机制:先执行同步代码,然后在每个「宏任务」结束前,会执行完所有的「微任务」。之后,才会继续下一个「宏任务」


🔍 深入理解「微任务」执行时机

「微任务」总是在当前「宏任务」结束后,下一个「宏任务」开始前执行。这意味着,如果你在一个「宏任务」的执行过程中不断地生成新的「微任务」,它们会继续累积,并在当前「宏任务」结束前全部执行完毕,而不是等待到下一轮事件循环。

「微任务」的执行流程特点

  1. 即时性「微任务」在当前执行栈清空后立即执行,这意味着它们不会受到后续「宏任务」的延迟影响。
  2. 连续执行:如果在执行一个「微任务」的过程中又产生了新的「微任务」,这些新产生的「微任务」不会等待当前循环结束,而是在当前「宏任务」「微任务」队列中继续累积,并在当前「宏任务」结束前依次执行完毕。这一过程会持续到「微任务」队列为空。
  3. 可能导致“任务饿死”:极端情况下,如果不断产生「微任务」而不让 JavaScript 引擎有机会处理「宏任务」,可能会导致浏览器或Node.js环境中的其他任务长时间得不到执行,表现为界面冻结或服务器响应迟缓等问题,这种情况称为“任务饿死”。

示例代码加深理解:

console.log('Start');function createMicrotask() {Promise.resolve().then(() => {console.log('Microtask');createMicrotask(); // 递归创建新的微任务});
}createMicrotask();console.log('End');

在这个例子中,createMicrotask函数通过Promise的.then方法创建了一个「微任务」。该函数在被调用时会打印"Microtask",并且递归调用自身再次创建一个新的「微任务」。由于「微任务」会在当前「宏任务」结束前全部执行完毕,因此在"End"被打印之后,控制台会连续输出多行"Microtask",直到达到JavaScript引擎的调用栈限制或其他停止条件为止。这进一步说明了「微任务」的即时执行和累积效应。


💡 实践建议

1. 避免过度依赖「微任务」

  • 使用场景选择:明确区分哪些操作更适合放在「微任务」中执行,如DOM更新后的立即操作、异步操作的快速回调等。对于需要一定延迟或不急于立即执行的任务,则可以考虑放入「宏任务」队列
  • 递归与循环控制:如示例代码所示的递归「微任务」调用,务必设置合理的退出条件,防止无限递归导致堆栈溢出。

2. 控制异步逻辑复杂度

  • 模块化和分层:将复杂的异步逻辑拆分为多个小的、职责单一的函数或模块,每个部分专注于处理一类任务,这样既便于阅读也易于维护。
  • 使用Async/Await:在支持的环境中,使用async/await语法糖可以极大地简化异步代码的编写,使其更像同步代码,提高可读性。

3. 注意异步操作对UI更新的影响

  • UI更新策略:虽然「微任务」先于UI渲染执行,但频繁的DOM操作或样式更改仍可能引起页面重排和重绘,影响性能。尽量批量处理DOM修改,或者利用requestAnimationFrame来协调UI更新,确保动画流畅。
  • 用户交互响应:确保关键的用户交互逻辑(如点击事件处理)能够迅速响应,避免在这些处理中嵌套过多「微任务」,以免阻塞UI线程,造成应用无响应的感觉。

4. 性能监控与调试

  • 工具利用:利用Chrome DevTools等开发者工具的Performance面板监控应用运行时的「宏任务」「微任务」调度情况,识别潜在的性能瓶颈。
  • 压力测试:在不同的设备和网络环境下进行压力测试,模拟高负载场景,检查「微任务」是否导致了不可接受的延迟或性能下降。

遵循这些建议,可以帮助开发者有效地平衡异步处理的需求与性能优化,确保应用程序的健壮性和用户体验。


🎖 总结

  • JavaScript事件循环是其异步编程模型的基础,它确保了非阻塞IO操作和高并发能力,让浏览器或Node.js环境下的JavaScript应用能高效运行。

  • 宏任务(Macrotasks),如setTimeoutsetIntervalI/OUI rendering等,构成了事件循环的主干,它们在每次循环的开始执行,控制着程序的主要流程。

  • 微任务(Microtasks),例如Promise的回调(then/catch/finally)、MutationObserverprocess.nextTick(在Node.js中)等,紧随当前「宏任务」之后执行,且在下一次「宏任务」开始前尽可能多地清空「微任务」队列,这使得「微任务」适用于需要立即响应的场景。

理解并正确利用「宏任务」「微任务」的执行机制,对于避免常见的异步陷阱、优化应用性能、以及实现流畅的用户体验至关重要。无论是处理复杂的异步逻辑、提升UI交互的即时反馈,还是维护代码的可读性和可维护性,掌握这一核心知识都能让你的JavaScript编程技能更上一层楼。

通过本文,希望你已经掌握了JavaScript事件循环的核心概念,理解了「宏任务」「微任务」的区别及其执行顺序。记住,掌握事件循环机制对于编写高效、可预测的异步JavaScript代码至关重要。现在,你已经具备了驾驭JavaScript异步魔法的能力,去创造更流畅的用户界面和强大的Web应用吧!🌈


🔗 相关链接

  • JavaScript 中的 Class 类
  • JavaScript中call、apply与bind的区别
  • JavaScript 垃圾回收机制深度解析:内存管理的艺术

在这里插入图片描述

这篇关于深入理解JavaScript事件循环Event Loop:宏任务与微任务的奇幻之旅的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/980372

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下