JavaScript 之【事件循环】机制的深入浅出解读

前言:

JavaScript 作为一种单线程的开发语言,在执行的时候会有特定的风格,本章节以 JS 单线程的特点为引入,详细分析讲解了 JS 的事件循环机制

目录

    • 什么是【进程】
    • 什么是【线程】
    • 浏览器拥有哪些进程和线程
    • 【渲染主线程】的工作模式
      • 结论:
    • 异步的概念:
    • 任务执行的优先级?
      • 扩展小问答:

什么是【进程】

程序在运行的时候,需要占用一定的系统运行内存我们就可以把这块内存空间简单的理解为进程

JavaScript 之【事件循环】机制的深入浅出解读_第1张图片
每个应用在运行时至少会有一个进程空间,进程之间 相互独立,这样的设计背后考虑的是,维持一个大运行环境的稳定性,比如说,如果,QQ因为程序错误,造成崩溃了,那么由于进程空间 之间的相互独立,并不会影响到 weixin,反之如果不相互独立,有程序一但出现崩溃,就会影响到整个运行时的程序。当然,如果程序间需要进行通讯的话,则需要建立在双方“达成共识”的条件下方可互通讯。


什么是【线程】

有时候生活中,我们也常常听到过多线程的说法,大家的理解说法不一,那么对于多线程 的正确理解你知道吗?

当进程空间开辟后,就开始运行代码程序了,我们可以理解为,最后真正上手执行代码的 “人”,我们称之为线程

进程和线程的关系属于一种 包含的的关系,有了进程空间,才能诞生线程来处理具体的事务。

⼀个进程至少有⼀个线程,所以在进程开启后会自动创建⼀个线程来运行代码,该线程称之为主线程(当主线程结束时,就意味着整个程序运行结束了)

如果程序的业务能力过于复杂,为了减轻主线程的压力,主线程就会启动更多的子线程来执行代码,从而分担主线程的负载。所以⼀个进程中可以包含多个线程,这也就是,我们所听到的多线程的概念。

JavaScript 之【事件循环】机制的深入浅出解读_第2张图片


以上介绍了 有关进程线程的概念,要想更加透彻的理解 事件循环机制 的执行逻辑,我们需要先了解清楚 浏览器的进程模型概念

浏览器拥有哪些进程和线程

浏览器是⼀个多进程多线程的应用程序。现如今的浏览器为了适应多场景高复杂化的业务功能,其内部工作原理已经变得极其复杂了。为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。

JavaScript 之【事件循环】机制的深入浅出解读_第3张图片
补充: 在浏览器诸多进程中,只有浏览器进程主进程,浏览器初始化启动时,只有浏览器进程,其他线程,如:网络进程,渲染进程,都是浏览器进程去调度开辟的。

图例中的三种,为浏览器中使用频率较高,具有代表性的进程。当然浏览器的进程,远远不止于图例中的三种…

进程名 进程描述
浏览器进程 主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启多个线程处理不同的任务。
网络进程 负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络事务。
渲染进程 渲染进程启动后,会开启⼀个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。 渲染进程也是本章节重点讲解的内容。

默认情况下,浏览器会为每个标签页开启⼀个新的渲染进程,以保证不同的标签页之间不相互影响


【渲染主线程】的工作模式

在了解了浏览器的进程模型后,我们得知了渲染进程,是和我们前端开发者,最密切相关的一个进程栈,我们编写的全部代码,都需要通过渲染进程来执行,所以接下来我们就重点聊聊,在浏览器中,渲染主线程的工作方式。

渲染主线程 是浏览器中最繁忙的的线程,他需要处理的任务包括,但是不仅限于:

  • 解析HTML
  • 解析CSS
  • 执行全局 JS 代码
  • 处理图层

针对要处理的诸多任务,渲染主线程面临的最大问题,就是如何去高效的调度这些任务,并进行的合理的资源分配?

如下面临的问题:

  • 我正在执行⼀个 JS 函数,执行到⼀半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
  • 当前正在执行⼀个 JS 函数,执行到⼀半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
  • 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪⼀个呢?

针对于这个问题,渲染主线程想出了设计出了一个绝妙的处理方式创建任务队列,按序排队执行

如下图例:
JavaScript 之【事件循环】机制的深入浅出解读_第4张图片

  1. 在渲染主线程启动的时候,主线程会进入到到一个无限循环的的执行模式中
  2. 在进行每一次循环时,都会检查消息队列池中当前是否有待执行的任务存在,如果有,则按顺序取出第一个去执行,执行完成后再次进入到下一次循环;如果任务池这时候没有待执行的任务时,主线程就会进入到休眠模式
  3. 其它的所有线程(包括其它进程的线程)可以随时的向消息队列池添加任务,新添加的任务会依次追加在任务池的末尾,在添加新任务的时候,如果这时主线程是休眠的状态,则会立即将其唤醒,继续执行循环拿取并执行任务。

扩展贴上一张Chrome 浏览器源码图例,如图34行开启了一个For 无限循环,对应了渲染主线程的循环执行。

JavaScript 之【事件循环】机制的深入浅出解读_第5张图片

结论:

这样一来,整套循环执行事务的流程,就被称之为 【事件循环】或消息循环


异步的概念:

在了解清楚了,浏览器的事件循环机制后,紧接着,就引出了 JS 的一个,新的知识点概念,叫做 异步函数

当我们的代码在执行的过程中,经常会遇到一些 无法立即执行的 任务,比如说:

  • 计时器定点完成后才需要执行的任务:setIntervalsetTimeout
  • 需要在网络通信完成后执行的任务:fetchXMLHttpRequest
  • 需要在用户交互操作触发后再去执行的任务:addEventListener

如果当渲染主线程在执行这些任务的时候,一直处于等待的状态,等待这些任务的触发时机到达,那么就会导致渲染主线程长时间处于【阻塞】的状态,从而可能会导致浏览器面临卡死崩溃的处境

同步执行图例:

JavaScript 之【事件循环】机制的深入浅出解读_第6张图片

以上图例演示了,如果 如果当渲染主线程对所有的代码都采取同步化的执行方式,那么在遇到这种非立即执行的代码时,主线程就只能按顺序依次执行,在这期间,主线程会造成严重的阻碍现象后续的事情不能及时的响应执行,页面上会呈现出卡顿延迟的表现

结论

渲染主线程承担着极其重要的工作,无论如何都不能阻塞!

对此:浏览器采用了 异步 执行的方式来解决这个问题。

异步执行图例
JavaScript 之【事件循环】机制的深入浅出解读_第7张图片

使用异步的执行方式:渲染主线程将永不阻塞

JavaScript 之【事件循环】机制的深入浅出解读_第8张图片


任务执行的优先级?

其实此处的标题,严格意义上,算是一个伪命题,因为严格意义上来说,任务是没有优先级的,所有的任务优先等级都是一样的。遵循着先进先出的规则执行=>>>>>>但是:【消息队列池】是有优先级划分的

扩展补充: 在过去,对浏览器任务队列的理解是,浏览器只有两个任务队列:一个是普通的宏任务队列,另一个是优先级高的:微任务队列。但是如今这种模式,已经被一种新的模式取代了,因为如今随着浏览器场景的越来越复杂化,两种任务队列已经满足不了现在的需求了

如今根据 W3C 最新的解释是: 点击查看W3C官方解释

  • 抛弃了过去 宏任务的说法
  • 现在每一个任务都有一个任务类型,并且同一个类型的任务,必须排在同一个消息队列,同时也提供了:不同类型的任务可以分属于不同的队列,在每一次事件循环中,浏览器可以根据实际的情况,智能调度从不同的队列中取出任务执行(这一点,随着不同的浏览器厂商,甚至是,不同时期的版本,智能调度算法都不一样,了解即可。)
  • 浏览器必须提供一个【微队列池】微队列中的任务执行优先级要高于其他所有类型队列池中的任务

附上:Chrome 浏览器源码片段图(部分):每一个字段都代表了一个任务类型。
JavaScript 之【事件循环】机制的深入浅出解读_第9张图片

在目前 Chrome 浏览器的实现中,至少包含了以下几种任务队列池:

  • 微任务队列池:用于存放需要被立即执行的任务 优先级:【最高】
  • 交互任务队列池:用于存放用户与页面交互后产生的事件处理任务 优先级:【高】
  • 延时任务队列池:用于存放计时器时间到达后待执行的回调任务 优先级:【中】
  • 网络任务队列池:用于存放网络资源请求和服务端交互处理的事务 优先级:【中】
  • …等等

对于微任务队列 的理解,可以总结为:当渲染主线程开始拿取任务开始执行的时候,倘若这时候,有很多不同类型的任务池中,都存在有排队待执行的任务,渲染主线程,会优先去,执行微任务池中的任务,直到将微任务池清空后,才会去自由调度的执行其他类型任务池中的任务。

添加任务到微任务队列的主要使用方式是:Promise,MutationObserver

Promise.resolve().then(() => {
    console.log("微任务优先级最高");
})

JavaScript 之【事件循环】机制的深入浅出解读_第10张图片

浏览器还有很多其它类型队列,由于和我们前端开发关系不大,所以不做过多阐述.


扩展小问答:

阐述一下 JS 的事件循环机制

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息队列中取出第⼀个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是⼀种更加灵活多变的处理方式。根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同⼀个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在⼀次事件循环中,由浏览器自行决定取哪⼀个队列的任务。但浏览器必须有⼀个微队列,微队列的任务⼀定具有最高的优先级,必须优先调度执行。

  • 单线程是异步产生的原因
  • 事件循环是异步的实现方式

JS 中的计时器能做到精确计时吗?为什么?

不行因为:

  1. 计算机硬件没有原子钟,无法做到精确计时。
  2. 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差。
  3. 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差。
  4. 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差

本章节 我们详细的概述分析了,基于JS为一种单线程语言的机制背后,其运行在浏览器环境中的时候,它的事件循环处理机制的逻辑,以及通过事件循环机制,也很好的理解了异步函数的概念,本章节内容不多,却是满满的干货,希望认真读完本章的小伙伴,对这一节的知识点能有新的理解。


‍♂️ 博主座右铭:向阳而生,我还在路上!
——————————————————————————————
博主想说:将持续性为社区输出自己的资源,同时也见证自己的进步!
——————————————————————————————
‍♂️ 如果都看到这了,博主希望留下你的足迹!【收藏!点赞!✍️评论!】
——————————————————————————————

你可能感兴趣的:(JS,高阶,javascript,网络,chrome,前端)