事件循环机制

这篇具有很好参考价值的文章主要介绍了事件循环机制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

基本概念

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

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

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

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

浏览器中的事件循环

我们都知道,在js中随着运行主线程中的代码,而主进程中的代码会不断调用外部浏览器的各种API,Ajax或者是代码中的定时器等各种异步事件,肯定有⼀个先来后到的排队问题。决定这些事件如何排队触发的机制,就是事件循环。这个排队⾏为以 JavaScript 开发者的⻆度来看,主要是分成两个队列,即下文所说的任务队列和微任务队列。
需要注意的是,虽然为了好理解我们管这个叫队列 (Queue),但是本质上是有序集
合 (Set)
浏览器整体事件循环模型:
事件循环机制

由上图我们知道除了主线程之上的任务,在异步队列中也有微任务队列和(宏)任务队列(因为html标准文档说的是Task Queue所以下面说的是任务队列,可以类比就是宏任务队列)在浏览器中的事件循环模型中任务队列的模型如下如所示:
事件循环机制
由上图我们可知:

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

任务队列

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

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

可以观察到,这些外部的事件源可能很多,为了⽅便浏览器⼚商优化,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. 6个阶段中的每个阶段都是一个先进先出的任务队列
  2. 会依次循环每个阶段(timers ->pending -> idl -> poll -> check -> close callbacks)循环到该阶段时,会把该阶段中的任务队列所有任务执行完
  3. 取出所有微任务并执行完
  4. 再执行任务队列中的任务
  5. 清空微任务队列中的任务

阶段概述

  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 事件循环文章来源地址https://www.toymoban.com/news/detail-479164.html

到了这里,关于事件循环机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Qt 底层之事件驱动系统】深入理解 Qt 事件机制:主事件循环与工作线程的交互探究,包括 QML 的视角

    在探讨 Qt 的世界时,我们不仅是在讨论一种编程框架,更是在探索一种将复杂技术细节隐藏于幕后、让开发者专注于创造性工作的艺术形式。正如著名的计算机科学家 Edsger Dijkstra 所言:“简洁是复杂性的先决条件。” 在这一章节中,我们将探讨 Qt 事件机制的基础概念,这

    2024年02月22日
    浏览(43)
  • 谈一谈浏览器与Node.js中的JavaScript事件循环,宏任务与微任务机制

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

    2024年02月12日
    浏览(38)
  • Kafka系列(一)【消息队列、Kafka的基本概念、Kafka的工作机制、Kafka可满足的需求、Kafka的特性、Kafka的应用场景】

    转自《Kafka并不难学!入门、进阶、商业实战》 1. 消息队列的来源 在高并发的应用场景中, 由于来不及同步处理请求,接收到的请求往往会发生阻塞。 例如,大量的插入、更新请求同时到达数据库,这会导致行或表被锁住,最后会因为请求堆积过多而触发“连接数过多的异

    2024年02月20日
    浏览(25)
  • 说一下JVM类加载机制?

    Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。 在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的

    2024年02月21日
    浏览(30)
  • 介绍一下js垃圾回收机制

    JavaScript中的垃圾回收机制负责自动管理内存,回收不再使用的对象所占用的内存空间。在JavaScript中,开发者不需要显式地分配和释放内存,垃圾回收器会自动完成这些操作。 以下是关于JavaScript垃圾回收机制的一些关键概念: 内存生命周期 :JavaScript内存生命周期包括分配、

    2023年04月11日
    浏览(34)
  • 详细解释一下Spring是如何解决循环依赖问题的

    我们都知道,如果在代码中,将 两个或多个Bean互相之间持有对方的引用就会发生循环依赖。循环的依赖将会导致注入死循环 ,这是Spring发生循环依赖的原因 1. 互相依赖: A依赖B , B依赖A , 他们之间形成了循环依赖 2. 间接依赖: A依赖B ,B依赖C , C又依赖A,形成了循环依

    2024年02月09日
    浏览(30)
  • 【Redis】聊一下Redis的哨兵机制

    在上一篇文章中,我们学习了数据库的Redis的主从集群复制模式,如果从库出现问题,那么其他主从库还可以处理读写请求,但是如果主库宕机,写请求从库处理不了,整个系统就不可用了,虽然只处理只读请求,显然是不符合业务需求。 如上图中所示当主库出现异常的,如

    2024年02月07日
    浏览(28)
  • Node基础概念,先了解一下

    Nodejs是基于Chrome V8引擎的服务器端JavaScript运行环境,也就是说可以在浏览器之外的主机上运行JavaScript。 Nodejs有三部分组成:标准库、中间层和底层库。 标准库:是给开发人员直接调用的API,比如HTTP模块; 中间层:为什么要有中间层呢?主要是因为NodeJS底层采用C语言来实现

    2024年02月07日
    浏览(27)
  • 浏览器事件循环(事件轮询)

    浏览器事件循环(Browser Event Loop)是浏览器用于处理用户输入、网络请求、渲染和其他异步事件的机制。这个循环确保了 JavaScript 代码的执行是非阻塞的,允许浏览器同时处理多个任务,从而提高用户体验。以下是浏览器事件循环的详细说明: 调用栈: 当一个 JavaScript 脚本

    2024年01月19日
    浏览(40)
  • 学习一下Java的ArrayList和contains函数和扩容机制

    在Leetcode上做题写了两种暴力解法,但是执行效率上不太一样。 时间上差很远,内存虽然差不多但是前者击败30%,后者击败94%。这两种解法区别是用一条 ArrayList 还是两条来存数据,所以contains虽然执行次数一样但是检测的长度上不一样,而且 ArrayList 的扩容次数也不一样,所

    2024年02月08日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包