谈一谈浏览器与Node.js中的JavaScript事件循环,宏任务与微任务机制

news2024/11/15 21:37:16

JavaScript中的异步代码

JavaScript是一个单线程非阻塞的脚本语言。这代表代码是执行在一个主线程上面的。但是JavaScript中有很多耗时的异步操作,例如AJAX,setTimeout等等;也有很多事件,例如用户触发的点击事件,鼠标事件等等。这些异步操作并不会阻塞我们代码的执行。例如:

let a = 1;
setTimeout(() => {
  console.log('->', a)
}, 10);
a = 2;
// 输出 -> 2

可以看到,上述代码在浏览器中执行时,遇到setTimeout操作,并没有阻塞等待异步操作的结束再继续执行代码,而是先继续执行后面的代码。等异步操作结束后,浏览器再回来执行异步回调中的代码。因此,上述代码的console.log输出时,a的值已经变为了2。

这些异步非阻塞的实现,就是靠Javascript中的事件循环机制。

JavaScript中的线程

上面说到JavaScript是一个单线程的语言,这句话并不完全对。单线程指的是代码在一个主线程中运行,但是代码所触发的任务不一定在主线程运行。除了执行代码的线程之外,执行JavaScript的环境中还包含其他很多线程。其中浏览器的线程与Node.js中的线程也不相同。

浏览器中的线程

  • JS主线程
    负责运行JavaScript代码。
  • GUI渲染线程
    渲染浏览器页面,解析HTML,CSS,构建DOM树,布局和绘制页面等等。
  • 事件监听线程
    负责监听触发的各种事件,放入事件循环中。
  • HTTP请求线程
    负责处理各类网络请求。
  • 定时触发器线程
    为setInterval,setTimeout定时触发操作等操作进行定时计数的线程。

其中GUI线程和JS线程是互斥的,即JS线程执行时,GUI线程会被挂起,即不能执行。反之GUI线程执行时,JS线程也不能同时执行。

浏览器中的进程

上面的线程实际上都在浏览器中的渲染进程中包含。一个浏览器要想正常运行,只做上述的操作是不够的。我们以Chrome为例,列举一个浏览器运行所需要的进程。

  • 浏览器进程
    负责网页外的界面功能,例如地址栏,书签等等。
  • GPU进程
    负责使用GPU渲染界面。
  • 网络进程
    负责网络相关的请求处理。
  • 插件进程
    负责浏览器插件运行。
  • 渲染进程
    负责网页内页面展示相关的操作,即上一节浏览器中的线程包含的所有线程都在这个进程中执行。

一个浏览器可以拥有多个标签页,在不同的标签页中,除了渲染进行之外,都是共享的。即我们打开一个新的标签页时,会产生一个新的渲染进程。(当在原标签页中打开新标签页,且属于同一个域则共享一个渲染进程)

进程与线程的关系

上面我们了解了浏览器中的进程和线程,有些同学就会有疑问,为什么要设立这么多的进程和线程?

进程是操作系统分配资源的基本单位,而线程是CPU任务调度和执行的基本单位。

简单理解下就是一个完整的应用程序是以进程为单位的,即至少有一个进程。而一段程序/代码在CPU的独立执行则至少以线程为单位。不同的进程和不同的线程都可以并行运行。

一个进程可以包含很多个线程,多个线程共享一个进程的资源(比如内存)。当一个进程崩溃后不会影响其他进程,但是当一个线程崩溃,它所在的整个进程都会崩溃掉,这个进程内的其他线程也会崩溃。

因此,为了同时并行执行代码和异步请求,浏览器中的渲染进程包含很多线程来并行运行任务。而为了让不同标签页的网页不互相影响,不同标签页拥有独立的渲染进程。这样即使某个网页崩溃,也不会影响其他标签页。

Node.js中的线程

  • JS主线程
    负责运行JavaScript代码。
  • libuv的异步I/O线程池
    负责实现事件循环和异步IO等操作,在不同操作系统的具体实现方式不同。
  • 用户创建的线程

上述这些进程和线程的说明也仅仅是进行了抽象和简化,事实上浏览器和Node.js中的进程和线程数要更多,处理也更复杂。

宏任务与微任务

Javascript中的异步任务大致可以分为两种:宏任务和微任务。宏任务和微任务的执行顺序和优先级是不同的,具体的执行顺序问题我们在事件循环中描述,这里先来看一下,哪些操作属于宏任务,哪些属于微任务。这里仅仅是简单介绍,更详细的要在了解事件循环之后说明。

宏任务

任务浏览器Node.js描述
setTimeout在指定的毫秒数后调用函数
setInterval定时调用函数
script标签整体代码块
I/O请求例如文件请求,网络请求等
DOM事件例如点击事件,hover事件等
requestAnimationFrame浏览器重绘前更新动画
postMessageiframe跨域通信
MessageChannel管道通信
setImmediate一次事件循环执行完毕调用

微任务

任务浏览器Node.js描述
Promise中resolve和reject回调
async函数中的await异步函数
MutationObserver监听DOM变动触发
process.nextTick当前任务结束后执行

事件循环

与上面进程与线程的介绍一样,在浏览器中与Node.js中实现循环的方式也并不相同。下面我们来分别简单介绍一下。注意,这仅仅是对执行逻辑的抽象和总结,实际上浏览器和Node.js中的实现要更复杂。

浏览器中的事件循环

浏览器中的事件循环可以分为两个队列,宏任务队列和微任务队列。具体的任务执行顺序如下:

  1. 解析HTML中遇到script标签,开始执行第一个宏任务。
  2. 在宏任务执行中遇到宏任务,执行其中的请求(例如网络请求,定时器),在请求完成后将回调放入宏任务队列中。
  3. 在宏任务执行中遇到微任务,暂不执行回调,而是放入微任务队列中。
  4. 宏任务执行完成。开始依次执行微任务队列中的任务。
  5. 微任务执行中遇到宏任务或者微任务,处理方式同上,分别放入各自的队列中。
  6. 微任务队列清空后,开始执行宏任务队列中的下一个任务。

在事件循环的流程中,微任务的优先级实际上更高,执行完一个宏任务之后,要执行微任务队列中的所有任务。

为什么要区分宏任务和宏任务,优先级也不同

因为不同任务的开销不同,有的任务需要调用不同的线程甚至进程,有的任务需要等待请求返回甚至定时。

  1. 如果将全部的任务同步执行,那些耗时较久的任务会阻塞,造成整个页面加载缓慢。假设有请求A耗时10秒,请求B耗时20秒,如果同步执行,需要耗费30秒。如果将请求由其它线程实现,回调放入宏任务,则执行流程变为:执行代码->碰到A请求,其他线程异步等待返回->继续执行代码->碰到b请求,其他线程异步等待返回。A和B就实现了异步请求,回调被分别放入宏任务,等待下次事件循环。耗时间为20秒。
  2. 为什么微任务的优先级更高?因为微任务大部分是耗时不太久,不需要等待其他线程/进程等待完成通知的。因此,微任务相当于在宏任务的基础上进行了“插队”,拥有更高的优先级,也提高了页面的响应速度。

为什么script标签是宏任务呢?

  1. script标签可能需要异步请求获取,例如<script src="myscripts.js"></script>
  2. script标签是嵌入在HTML中的,浏览器需要将HTML中的script标签解析出来供执行,这个步骤需要耗费一定的时间。

浏览器事件循环的更多说明

WHATWG(网页超文本应用技术工作小组)在官网对事件循环和任务队列做出了更详细的说明和解释,可以作为参考:说明文档。在新的说明中,任务的分类和事件循环已经有了部分区别,这里简要说一下,更多还请直接查看文档:

  1. 事件循环不一定对应于多线程。例如多个事件循环可以在单个线程中协作调度。
  2. 任务队列并不是一个严格的队列,而是一个集合。每次从队列中取出一个可以被执行的任务,而不是选取第一个任务(可能该任务还在阻塞中)。
  3. 宏任务队列有多个,不同类型的任务(任务源)放置在不同的任务队列中。具体的选取规则浏览器根据实际情况确定。

Node.js中的宏任务队列

Node.js的官网给出了事件循环的文档。它的事件循环要比浏览器的看起来复杂一些。下面是Node.js的宏任务队列。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Node.js的宏任务队列并不是一整个队列,而是根据事件类型做出了区分,分为了六个队列,依次执行:

  1. timers 定时器队列,执行定时器的回调
  2. pending callbacks 挂起的回调函数,用于某些系统回调
  3. idle, prepare 仅在内部使用
  4. poll 执行I/O事件回调
  5. check setImmediate回调
  6. close callbacks close事件的回调,例如 socket.on('close', ...)

其中我们的大部分宏任务回调都会在poll阶段执行,除了timerscheckclose callbacks阶段的特殊回调。每个宏任务队列都有自己的微任务队列。

在这里插入图片描述

Node.js事件循环的流程

  1. 首先执行主线代码,遇到宏任务就分配到对应的宏任务队列中,微任务也划分到主线的微任务队列中,直到执行完毕。
  2. 执行主线代码的微任务队列中的所有任务。
  3. 没有宏任务则执行结束,有则开始事件循环。在事件循环中,按照上述的6个宏任务队列依次执行。下面的步骤是单个队列中的流程。
  4. 在单个宏任务队列中,选择一个宏任务执行。如果执行中遇到新的宏任务就分配到对应的宏任务队列中。遇到微任务就放到该宏任务的微任务队列中。
  5. 一个宏任务执行完毕后,执行process.nextTick中的回调(如果有)。
  6. 执行当前宏任务的微任务队列中的任务,直到微任务队列清空。
  7. 在上面的单个宏任务队列中,再选择一个宏任务执行。直到当前宏任务队列清空或者到达上限。
  8. 选择下一个宏任务队列执行。

6个宏任务队列都执行完毕,才叫做一次事件循环执行完毕。

Node.js的11版本之前的区别

其中,在Node.js的11版本之前,宏任务和微任务的执行关系与上述流程不同:

每个宏任务队列有一个微任务队列。在单个宏任务队列中,首先执行完所有的宏任务,如果遇到微任务就放到微任务队列中。当单个宏任务队列中的所有宏任务执行完毕后,再执行该宏任务队列的微任务队列。

对比执行流程的区别,可以看到Node.js的11版本提高了微任务队列中的优先级,让Node.js中微任务队列的优先级和浏览器中的表现类似。而process.nextTick可以看做是一个比微任务更高优先级的钩子。

注意

  • setTimeout的时间即使设置为0,也会有一个最小时间,因此它与setImmediate谁更早执行不一定。
  • 并不是所有回调函数都是异步的。例如new Promise(fun)中的回调是同步执行,在回调中遇到resolve(), reject()等才是微任务异步执行的。

参考

  • JavaScript 之事件循环 (Event Loop)
    https://xie.infoq.cn/article/921841837025748baac847030
  • The Node.js Event Loop, Timers, and process.nextTick()
    https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick
    https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick
  • 深入理解浏览器中的进程与线程
    https://juejin.cn/post/6991849728493256741
  • 这一篇浏览器事件循环,可能会颠覆部分人的对宏任务和微任务的理解
    https://juejin.cn/post/7259927532249710653
  • HTML Living Standard (event-loops)
    https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
  • 阿里一面:熟悉事件循环?那谈谈为什么会分为宏任务和微任务
    https://juejin.cn/post/7073099307510923295
  • node.js事件循环简单理解——定时器,process.nextTick()等
    https://blog.csdn.net/qq_46561394/article/details/123172336
  • 手摸手带你彻底掌握,任务队列、事件循环、宏任务、微任务
    https://juejin.cn/post/6979876135182008357

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/909230.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

javaScript:对函数的认识与应用

目录 一.前言 二.函数介绍 A.函数的分类 1.自定义函数 示例 2.匿名函数 声明匿名函数 计时器也是匿名函数 3.立即执行函数 解释 示例 B.函数的返回值 没有参数&#xff0c;没有返回值的函数 示例 没有参数&#xff0c;有返回值的函数 示例 有参数&#xff0c;有…

Transformer在医学影像中的应用综述-分割

文章目录 Transformers in Medical Imaging: A Survey摘要方法手工的方法基于卷积的方法基于Transformer的方法影像分割2D3D 多器官分割纯transformer混合Transformer单规模结构transformer在编码器中Transformer在编码器和解码器之间Transformer在编码器和解码器中Transformer…

【React】生命周期和钩子函数

概念 组件从被创建到挂载到页面中运行&#xff0c;再到组件不用时卸载的过程。 只有类组件才有生命周期。 分为三个阶段&#xff1a; 挂载阶段更新阶段销毁阶段 三个阶段 挂载阶段 钩子函数 - constructor 创建阶段触发 作用&#xff1a;创建数据 之前定义状态是简写&…

Datawhale Django 后端开发入门 Task05 DefaultRouter、自定义函数

一、DefaultRouter是Django REST framework中提供的一个路由器类&#xff0c;用于自动生成URL路由。路由器是将URL与视图函数或视图集关联起来的一种机制。Django REST framework的路由器通过简单的配置可以自动生成标准的URL路由&#xff0c;从而减少了手动编写URL路由的工作量…

五种消息模型简单说明

五种消息模型简单说明 RabbitMQ提供了6种消息模型&#xff0c;但是第6种其实是RPC&#xff0c;并不是MQ&#xff0c;因此不予学习。那么也就剩下5种。但是其实3、4、5这三种都属于订阅模型&#xff0c;只不过进行路由的方式不同。  我们通过一个demo工程来了解下RabbitMQ的…

代码随想录算法训练营(23/6/25)LeetCode 84.柱状图中最大的矩形

LeetCode 84.柱状图中最大的矩形 今天是算法训练营的打卡的最后一天&#xff0c;我开始觉得我能坚持下来&#xff0c;但因为个人原因&#xff0c;还有期末考试我花太多心思&#xff0c;打卡就一直断断续续&#xff0c;博客没怎么写&#xff0c;最终也写完了

ctfshow-web10 with rollup 绕过

0x00 前言 CTF 加解密合集CTF Web合集 0x01 题目 0x02 Write Up 基本方法&#xff0c;到处点一点&#xff0c;点到取消的时候&#xff0c;突然发现&#xff0c;可以下载一个文件&#xff1a; 看到这个源码&#xff0c;可以看到只能是通过满足下面的条件来拿到flag&#xff…

sql server 快速安装

目录标题 一、下载二、直接选择基本安装二、下载ssms&#xff08;数据库图形化操作页面&#xff09;三、开启sa账号认证&#xff08;一&#xff09;第一步&#xff1a;更改身份验证模式&#xff08;二&#xff09;第二步&#xff1a;启用 sa 登录 一、下载 下载地址&#xff1…

ModaHub魔搭社区:AI Agent在操作系统场景下的AgentBench基准测试

近日,来自清华大学、俄亥俄州立大学和加州大学伯克利分校的研究者设计了一个测试工具——AgentBench,用于评估LLM在多维度开放式生成环境中的推理能力和决策能力。研究者对25个LLM进行了全面评估,包括基于API的商业模型和开源模型。 他们发现,顶级商业LLM在复杂环境中表现出…

【leetcode 力扣刷题】链表基础知识 基础操作

链表基础知识 基础操作 链表基础操作链表基础知识插入节点删除节点查找节点 707. 设计链表实现&#xff1a;单向链表&#xff1a;实现&#xff1a;双向链表 链表基础操作 链表基础知识 在数据结构的学习过程中&#xff0c;我们知道线性表【一种数据组织、在内存中存储的形式】…

基于原生Servlet使用模板引擎Thymeleaf访问界面

我们常在Spring Boot项目中使用Thymeleaf模板引擎,今天突发奇想&#xff0c;尝试原生Servlet访问&#xff01; 说做就做 搭建完整的WEB项目 其中的大部分依赖都是后续报错 追加进来的 导入依赖 thymeleaf-3.0.11.RELEASE.jar 第一次访问 访问地址: http://localhost:8080…

利用屏幕水印学习英语单词,无打扰英语单词学习

1、利用屏幕水印学习英语单词&#xff0c;不影响任何鼠标键盘操作&#xff0c;不影响工作 2、利用系统热键快速隐藏&#xff08;ALT1键 隐藏与显示&#xff09; 3、日积月累单词会有进步 4、软件下载地址: 免安装&#xff0c;代码未加密&#xff0c;安全的屏幕水印学习英语…

Linux学习之ftp安装、vsftpd安装和使用

ftp需要两个端口&#xff1a; 数据端口 命令端口 ftp有两种模式&#xff1a; 被动模式&#xff1a;建立命令连接之后&#xff0c;服务器等待客户端发起请求。 主动模式&#xff1a;建立命令连接之后&#xff0c;服务器主动向客户端发起数据连接&#xff0c;因为客户端可能有防火…

6-2 使用函数求素数和

分数 20 全屏浏览题目 切换布局 作者 张高燕 单位 浙大城市学院 本题要求实现一个判断素数的简单函数、以及利用该函数计算给定区间内素数和的函数。 素数就是只能被1和自身整除的正整数。注意&#xff1a;1不是素数&#xff0c;2是素数。 函数接口定义&#xff1a; int p…

相关变化率的例子

如图&#xff0c;不解释。 很多物理学上的物理量&#xff0c;直接使用微分和导数来定义&#xff0c;因此可以不加证明的直接使用这些物理量。 解&#xff1a; d l 2 , d w 3 dl 2, dw 3 dl2,dw3 v l 2 w 2 , d v − 2 l d l 2 w d w 2 l 2 w 2 − 2 12 2 2 5…

内网渗透神器CobaltStrike之内网信息收集(九)

收集域内信息 Windows命令 查看网关的ip地址, DNS的ip地址、域名等等&#xff1a;shell ipconfig /all 查看当前主机所在的域: shell net view /domain 查看当前域的主机列表: shell net view 查看指定域的主机列表: shell net view /domain:[domain] 若beacon用户是域控, 则…

数据同步工具比较:选择适合您业务需求的解决方案

在当今数字化时代&#xff0c;数据已经成为企业的核心资产。然而&#xff0c;随着业务的扩展和设备的增多&#xff0c;如何实现数据的高效管理和同步成为了一个亟待解决的问题。本文将介绍几种常见的数据同步工具&#xff0c;并对比它们的功能、性能和适用场景&#xff0c;帮助…

二、9.硬盘驱动程序

文件系统是运行在操作系统中的软件模块&#xff0c;是操作系统提供的一套管理磁盘文件读写的方法和数据组织、存储形式&#xff0c;因此&#xff0c;文件系统&#xff1d;数据结构&#xff0b;算法&#xff0c;哈哈&#xff0c;所以它是程序。它的管理对象是文件&#xff0c;管…

互斥锁的概念,与部分接口

何为互斥 一种对共享数据的保护&#xff0c;防止多线程同时访问共享资源的时&#xff0c;数据混乱的问题。在互斥期间&#xff0c;保证执行流由并行改为串行。任何时刻&#xff0c;互斥保证有且只有一个执行流进入临界区&#xff0c;访问临界资源&#xff0c;通常对临界资源起…