事件循环机制

news2025/2/25 14:11:46

基本概念

聊一下事件循环机制,在开始这篇文章之前,先明确一个概念,js本身是没有事件循环这个定义的。是js被嵌入相应的执行环境(浏览器 / Nodejs),配合当前事件循环的组成部分,具体来说分下面两部分:

  1. 在浏览器环境中,事件循环是HTML标准中定义的,用于协调浏览器端的多种事件源的一种机制
  2. 在Nodejs环境中,遵循的事件循环是来源于Libuv

上面说的事件源是指各种交互,当我们打开一个网页可能触发各种各样的事件源,比如:

  • 用户交互: 鼠标,键盘,摄像头
  • 脚本:js
  • 渲染:HTML, DOM, CSS,图片
  • 网络请求: ajax
    这在HTML标准定义中可以查到相关资料:
    在这里插入图片描述
    而事件循环的目的其实就是提供了一种机制为了解决再当前环境中各种事件源之间怎么协作的问题。
    比如在浏览器端,在用户的浏览器中既有UI渲染,也有用户交互(鼠标,键盘),也有Ajax请求各种资源,等等。那怎么能让这些事情有条不紊的协作起来。让用户能够正常符合预期一样的使用我们开发的界面。
    在Nodejs服务端也是一样,有各种I/O操作,各种定时任务,服务请求,各种各样的计算,要让这么多任务有条不紊并高效的执行。
    这就是事件循环解决的问题,具体怎么在技术上实现,是各自浏览器厂商各自实现的。

浏览器中的事件循环

各种浏览器事件同时触发时,肯定有⼀个先来后到的排队问题。决定这些事件如何
排队触发的机制,就是事件循环。这个排队⾏为以 JavaScript 开发者的⻆度来看,
主要是分成两个队列,即下文所说的任务队列和微任务队列。
需要注意的是,虽然为了好理解我们管这个叫队列 (Queue),但是本质上是有序集
合 (Set)
在浏览器中的事件循环模型如下如所示:
在这里插入图片描述
由上图我们可知:

  1. 每次循环都只会处理一个外部任务
  2. 每次都会清空内部队列所有任务
  3. 至于是所有内容都渲染完,还是渲染到一半切出来回到第一步得看浏览器的具体实现。
  4. 对于一段js代码而言,js执行的第一个任务队列中的任务就是当前这个script脚本本身(即第一次是执行脚本本身)
    注意:微任务不一定在任务队列中的任务之前执行的,一定是先执行一次外部任务(首次任务队列中的任务,即脚本本身),然后才会清空当前的内部任务

任务队列

我看很多文章都称之为“宏任务”,可能是相对于下面所说的“微任务”而言的,我看HTML标准协议中的描述是task queues,这里就统一称之为任务队列。主要包括下面这些事件:

  1. DOM 操作 (⻚⾯渲染)
  2. ⽤户交互 (⿏标、键盘)
  3. ⽹络请求 (Ajax 等)
  4. History API 操作
  5. 定时器 (setTimeout 等)

可以观察到,这些外部的事件源可能很多,为了⽅便浏览器⼚商优化,HTML 标准中明确指出⼀个事件循环由⼀个或多个任务队列,⽽每⼀个任务事件源都有⼀个对应的队列。不同事件源的队列可以有不同的优先级(例如在⽹络事件和⽤户交互之间,浏览器可以优先处理⿏标⾏为,从⽽让⽤户感觉更加流程)。

微任务队列

在 HTML 标准中,并没有明确规定这个队列的事件源,通常认为有以下
⼏种:
• Promise 的成功 (.then) 与失败 (.catch)
• MutationObserver
• Object.observe (已废弃)

示例

Demo1

setTimeout(() => console.log(1), 0)

比如整个代码就只有这一行。那么背后按照时间模型执行的逻辑为:
第一次循环:

  1. 执行js脚本本身:调用浏览器的setTimeout这个API,将其注册的callBack函数(这里是() => console.log(1))注册到任务队列中。
  2. 取出所有微任务队列中任务并执行:这里没有
  3. 浏览器渲染:这里无

第二次循环

  1. 取出任务队列中的回调函数(() => console.log(1))执行
  2. 取出所有微任务队列中任务并执行:这里没有
  3. 浏览器渲染:这里无

整个任务就结束了

Demo2

<html>

<body>
  <pre id="render"></pre>
</body>
<script>
  const main = document.querySelector('#render');
  const callback = (i, fn) => () => {
    main.innerText += fn(i);
  };
  let i = 1;
  // 第一遍循环,外部任务执行script 中的脚本:i从0加到1000,并将1000个任务添加到任务队列中
  while (i++ < 1000) {
  	// 后续第n次循环分别执行一次对应的回调任务
    setTimeout(callback(i, (i) => '\n setTimeout> ' + i + ''))
  }
//  // 第一遍循环,外部任务执行script 中的脚本:i从1000加到2000,并将1000个任务添加到微任务队列中
  while (i++ < 2000) {
    Promise.resolve().then(callback(i, (i) => i + ','))
  }
  console.log(i)
  main.innerText += '[start ' + i + ' ]\n'
</script>

</html>

在这里插入图片描述

Demo3

// 函数定义
async function async1 () {
  // 第一遍循环外部任务时打印
  console.log('async1 start')
  // 第一遍循环外部任务时指定async2函数
  await async2()
  // await 后面的代码相当于 Promose.resolve().then() 中.then里面的函数
  // 在第一遍事件循环执行外部任务时加入到微任务队列
  console.log('async1 end')
}

// 函数定义
async function async2 () {
  // 第一遍循环外部任务时打印
  console.log('async2')
  // 如果这里有await 就会在第一遍事件循环执行外部任务时加入到微任务队列
}

// 第一遍循环外部任务时打印
console.log('script start')

// 加入到外部任务队列中,等待第二遍事件循环时执行
setTimeout(function () {
  console.log('setTimeout')
}, 0)

// 第一遍循环外部任务执行
async1()

new Promise(function (resolve) {
  // 第一遍循环外部任务时打印
  console.log('promise1')
  resolve()
  //resolve正常执行后打印promise2
  //.then之前的都会被打印出, 
  console.log('promise2')
}).then(function () {
  // .then函数和await 后面的内容会被加到对应的微任务队列中
  console.log('promise3')
})
console.log('script end')

结果为:
script start -> async1 start -> async2 -> promise1 -> promise2 -> script end -> async1 end -> promise3 -> setTimeout
事件循环第一遍任务:
script start -> async1 start -> async2 -> promise1 -> promise2 -> script end
事件循环第一遍微任务:
async1 end -> promise3
UI渲染阶段:无逻辑 (对应的Node环境直接无该阶段)
事件循环阶段第二遍任务:
setTimeout

Nodejs中事件循环

浏览器是将Js集成到HTML事件循环之中,与此对应的是Node.js 将js集成到libuv的 I/O循环之中。
简言之,二者都是将js集成到各自的环境中。但HTML(浏览器端)与libuv(服务端)面对的场景有很大的差异。比如:

  1. 事件循环的过程没有 HTML 渲染。只剩下了任务队列和微任务队列这两个部分。
  2. 任务队列的事件源不同。Node.js 端没有了⿏标、键盘,摄像头等外设但是新增了⽂件等 IO,与操作系统交互(通过libuv中转)。
  3. 微任务队列的事件仅剩下 Promise 的 then 和 catch

Node事件循环模型

node环境的任务队列的事件循环的6个阶段顺序是固定的(timers ->pending -> idl -> poll -> check -> close callbacks)

在这里插入图片描述
其主要逻辑如下:

  1. 先执行任务队列中的任务运行,任务队列会循环的在(timers ->pending -> idl -> poll -> check -> close callbacks)6个阶段任务中选择一个执行完。
  2. 取出所有微任务并执行完。
  3. 再执行任务队列中的任务(循环选一个外部任务)
  4. 清空内部任务

阶段概述

  1. timers: 此阶段执行由 setTimeout() 和 setInterval() 排序。
  2. pending callbacks: 执行 I/O 回调推迟到下一个循环 迭代。
  3. idle, prepare: 仅在内部使用。
  4. poll: 检索新的 I/O 事件; 执行与 I/O 相关的几乎任何回调(由“计时器”或 “setImmediate()”所设的紧邻回调除外); node 将在适当时机在此处暂停。
  5. check: setImmediate() 回调在此处被调用。
  6. close callbacks:一些关闭的回调函数,如:socket.on(‘close’, …)。

setImmediate

setTimeout(fn, 0) setTimeout的精度是毫秒级别的(0毫秒)。对计算机来说1毫秒可以执行很多任务(执行个几万次任务是很正常的),所以Node提供了一个setImmediate的API, 响应是微秒级别的。setImmediate 是有概率比setTimeout更早运行的。两个API在外部任务队列中的不同的阶段。如果是微秒级的(更快执行精度的触发器)可以考虑setImmediate。特别是在没有很大的I/O操作的情况下,很大概念在微秒精度下触发会比setTimeout执行更早。有一定的小概率是setTimeout先执行,其原因就是精度问题。

demo

setTimeout(()=>{
 console.log('setTimeout1');
 Promise.resolve().then(() => console.log('promise1'));
});
setTimeout(()=>{
 console.log('setTimeout2');
 Promise.resolve().then(() => console.log('promise2'));
});
setImmediate(() => {
 console.log('setImmediate1');
 Promise.resolve().then(() => console.log('promise3'));
});
setImmediate(() => {
 console.log('setImmediate2');
 Promise.resolve().then(() => console.log('promise4'));
});

在这里插入图片描述

process.nextTick()

您可能已经注意到process.nextTick() 在图示中没有显示,即使它是异步 API 的一部分。这是因为 process.nextTick()从技术上讲不是事件循环的一部分。相反,它都将在当前操作完成后处理nextTickQueue, 而不管事件循环的当前阶段如何。这里所谓的操作被定义为来自底层 C/C++ 处理器的转换,和需要处理的 JavaScript 代码的执行。

回顾我们的图示,任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。这可能会造成一些糟糕的情况,因为它允许您通过递归 process.nextTick()调用来“饿死”您的 I/O,阻止事件循环到达 轮询 阶段。
更多的内容请参考:https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick

参考文章

HTML事件循环
Nodejs事件循环
MDN 事件循环

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

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

相关文章

【基于容器的部署、扩展和管理】3.4 灰度发布和A/B测试

往期回顾&#xff1a; 第一章&#xff1a;【云原生概念和技术】 第二章&#xff1a;【容器化应用程序设计和开发】 第三章&#xff1a;【3.1 容器编排系统和Kubernetes集群的构建】 第三章&#xff1a;【3.2 基于容器的应用程序部署和升级】 第三章&#xff1a;【3.3 自动…

【C++】引用和右值引用

目录 1. 引用 1.1 引用的概念 1.2 引用的特性 1.3 引用的使用场景 1.3.1 作为参数 1.3.2 作为返回值 1.4 常量引用 1.5 引用和指针的区别 2. 左值和右值 3. 右值引用 3.1 右值引用的概念 3.2 左值持久&#xff1b;右值短暂 3.3 变量是左值 3.4 标准库move函数 1.…

docker使用与服务器上的可视化(ROS rviz等)

1.安装docker 安装docker&#xff1a;官网教程&#xff0c;按照官网命令一步步来即可。 添加当前用户到docker用户组&#xff1a; 【docker】添加用户到docker组&#xff0c;这样后面运行docker的时候前面不需要加sudo命令&#xff0c;否则运行docker的时候一直需要在前面加su…

开发人员必备的万能工具箱:He3

目录 1.简介2.安装3.详细功能清单4.常用快捷键5.使用示例5.1 测试正则表达式5.2 文本比较 He3官网&#xff1a; https://he3.app/zh/ 1.简介 今天&#xff0c;给大家推荐一款开发人员必备的万能工具箱&#xff1a;He3&#xff0c;这是一款免费的开发者工具箱&#xff0c;截至…

十分钟,零基础使用uniCloud完成后端管理系统搭建一

本文主要是通过uniCloud搭建后端管理系统&#xff0c;适合小白开发者、个人开发者&#xff0c;零后端基础&#xff0c;快速、低成本完成后端管理系统搭建。 还未创建uniCloud服务空间的开发者可以查看我的文章&#xff1a;Dcloud开发者注册&#xff0c;uniCloud服务空间创建。…

电脑显示屏不亮但是主机已开机?5种原因以及解决方案

电脑与我们的日常生活和工作密切相关&#xff0c;缺了它我们工作就很难正常展开。电脑使用久了&#xff0c;难免出现一些小问题&#xff0c;比如&#xff1a;电脑显示屏不亮但是主机已开机&#xff0c;这是什么原因造成的&#xff1f;我们应该怎么处理&#xff1f; 可能很多人…

随机数发生器设计(四)

随机数发生器设计&#xff08;四&#xff09;- DRNG 概述1 内部状态2 初始化函数3 SM3派生函数4 其他部分 概述 本示例DRNG设计参考了GM/T 0105 &#xff0c;基于SM3算法实现&#xff0c;内部功能接口包括初始化函数、重播种函数、输出函数和已知答案自测试函数&#xff0c;同…

正点原子STM32(基于HAL库)5

目录 SRAM 实验存储器简介SRAM 方案简介硬件设计程序设计程序流程图程序解析 下载验证 内存管理实验内存管理简介硬件设计程序设计程序流程图程序解析 下载验证 SD 卡实验SD 卡简介SD 物理结构命令和响应卡模式数据模式 SDIO 接口简介SDIO 主要功能及框图SDIO 的时钟SDIO 的命令…

《程序员必备品质》——沉稳1

目录 前言&#xff1a; 一.言论有分寸 1.1不抱怨不指责 1.2谈话时不触及别人的短 1.3学会装聋作哑 二.沉心静气 2.1先稳定情绪再解决问题 2.2急于求成则遇速不达 三.结尾 前言&#xff1a; 这周&#xff0c;我细细的读了一本书&#xff0c;还没精读完&#xff0c;不过…

华为OD机试真题 JavaScript 实现【最多几个直角三角形】【2023Q1 100分】

一、题目描述 有 N 条线段&#xff0c;长度分别为 a[1]-a[n]。 现要求你计算这 N 条线段最多可以组合成几个直角三角形&#xff0c;每条线段只能使用一次&#xff0c;每个三角形包含三条线段。 二、输入描述 第一行输入一个正整数 T (1< T< 100) &#xff0c;表示有…

开源WebRTC库放大器模式在采集桌面图像时遇到的DPI缩放与内存泄漏问题排查

目录 1、在非100%的显示比例下放大器采集到的桌面图像不全问题 1.1、通过manifest文件禁止系统对软件进行缩放 1.2、调用SetThreadDpiAwarenessContext函数&#xff0c;禁止系统对目标线程中的窗口进行缩放 1.3、使用winver命令查看Windows的年月版本 2、使用放大器模式遇…

4年经验去面试21k测试岗,看到这样的面试题我还是心虚了....

我是着急忙慌的准备简历——4年软件测试经验&#xff0c;可独立测试大型产品项目&#xff0c;熟悉项目测试流程...薪资要求&#xff1f;4年测试经验起码能要个21K吧 我加班肝了一页半简历&#xff0c;投出去一周&#xff0c;面试电话倒是不少&#xff0c;自信满满去面试&#…

Highcharts for Python crack

Highcharts for Python crack   Aligned the API with Highcharts Core v11.1. In particular, this includes:   Added AccessibilityPoint.description_format property.   Added support for .legend_symbol to plot options and series options.   Added .border_…

6月10日两练来了!

今天是2023年6月10日&#xff0c;每日两练来了&#xff01; 一。围城 题目 编程星球上有一片100*100的空地&#xff0c;啊哈沃德在此处建立了N座发电站&#xff0c;每座发电站占地位置1*1格。 为了防止源码巨人的攻击&#xff0c;啊哈沃德需要在所有发电站外围修建电网&#x…

redis第九章-Redis队列Stream、Redis6多线程详解

Redis队列Stream 前置说明&#xff1a;Redis5.0 最大的新特性就是多出了一个数据结构 Stream&#xff0c;它是一个新的强大的支持多播的可持久化的消息队列&#xff0c;作者声明 Redis Stream 地借鉴了 Kafka 的设计。 Redis Stream 的结构如上图所示,每一个Stream都有一个消息…

【浅谈DBA 最重要的素质---读书笔记】

&#x1f448;【上一篇】 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 【下一篇】&#x1f449; &#x1f53b;【来自DBA大佬的见解1】 对于一个准备进入 DBA 领域的人&#xff0c;我希望他勤奋、严谨、具有钻研精神及独立思考能力。…

Android应用程序进程的启动过程

Android应用程序进程的启动过程 导语 到这篇文章为止&#xff0c;我们已经简要地了解过了Android系统的启动流程了&#xff0c;其中比较重要的内容有Zygote进程的启动和SystemService以及Launcher的启动&#xff0c;接下来我们将要学习的是Android应用程序的启动过程&#xff…

Python爱好者的自我修养(1):简单输入与输出

Python简单输入与输出 1.输出1.1 简单输出1.2 转义字符1.2.1 定义1.2.2 常见的转义字符用法 2.输入3.温馨提示 终于…… 终于…… 我开始玩Python了 &#xff08;不是C不学了哈&#xff0c;C还是照更~&#xff09; 今天先来简单讲下输入和输出 1.输出 1.1 简单输出 输出的函…

【一篇让你学会】Web接口测试工具--Jmeter

关于Jmeter性能测试工具不再过多介绍。如果你要学习软件性能测试&#xff0c;那么多少应该会对它有所耳闻。 强烈建议阅读官方文档学习&#xff1a;http://jmeter.apache.org/index.html 还有比这个更权威更全面的介绍Jmeter工具使用的么&#xff1f; 不过&#xff0c;此处要介…

Win7批量执行Python文件

问题背景 平时都是用Pycharm跑代码&#xff0c;但是每次都需要在Configuration里修改Parameters&#xff0c;跑完一个才能重新修改跑下一个&#xff0c;很不方便&#xff0c;于是决定借助.bat文件实现批量执行。 困难一 电脑存在cmd闪退问题&#xff0c;之前一直逃避懒得解决…