前言:
JavaScript 作为一种
单线程
的开发语言,在执行的时候会有特定的风格,本章节以 JS 单线程的特点为引入,详细分析讲解了 JS 的事件循环机制
目录
- 什么是【进程】
- 什么是【线程】
- 浏览器拥有哪些进程和线程
- 【渲染主线程】的工作模式
- 结论:
- 异步的概念:
- 任务执行的优先级?
- 扩展小问答:
什么是【进程】
程序在运行的时候,需要占用一定的系统运行内存,我们就可以把这块内存空间简单的理解为进程
每个应用在运行时至少会有一个进程空间,进程之间 相互独立
,这样的设计背后考虑的是,维持一个大运行环境的稳定性,比如说,如果,QQ因为程序错误,造成崩溃了,那么由于进程空间
之间的相互独立,并不会影响到 weixin,反之如果不相互独立,有程序一但出现崩溃,就会影响到整个运行时的程序。当然,如果程序间需要进行通讯的话,则需要建立在双方“达成共识”的条件下方可互通讯。
什么是【线程】
有时候生活中,我们也常常听到过
多线程
的说法,大家的理解说法不一,那么对于多线程
的正确理解你知道吗?
当进程空间开辟后,就开始运行代码程序了,我们可以理解为,最后真正上手执行代码的 “人”,我们称之为线程
。
进程和线程
的关系属于一种包含的
的关系,有了进程空间,才能诞生线程来处理具体的事务。
⼀个进程至少有⼀个线程,所以在进程开启后会自动创建⼀个线程来运行代码,该线程称之为主线程
。(当主线程结束时,就意味着整个程序运行结束了)
如果程序的业务能力过于复杂,为了减轻主线程的压力
,主线程就会启动更多的子线程来执行代码,从而分担主线程的负载。所以⼀个进程中可以包含多个线程
,这也就是,我们所听到的多线程
的概念。
以上介绍了 有关进程
和线程
的概念,要想更加透彻的理解 事件循环机制
的执行逻辑,我们需要先了解清楚 浏览器的进程模型
概念
浏览器拥有哪些进程和线程
浏览器是⼀个
多进程多线程
的应用程序。现如今的浏览器为了适应多场景高复杂化的业务功能,其内部工作原理已经变得极其复杂了。为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。
补充:
在浏览器诸多进程中,只有浏览器进程为主进程,浏览器初始化启动时,只有浏览器进程,其他线程,如:网络进程,渲染进程,都是浏览器进程去调度开辟的。
图例中的三种,为浏览器中使用频率较高,具有代表性的进程。当然浏览器的进程,远远不止于图例中的三种…
进程名 | 进程描述 |
---|---|
浏览器进程 | 主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启多个线程处理不同的任务。 |
网络进程 | 负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络事务。 |
渲染进程 | 渲染进程启动后,会开启⼀个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。 渲染进程也是本章节重点讲解 的内容。 |
默认情况下,浏览器会为每个标签页开启⼀个新的渲染进程
,以保证不同的标签页之间不相互影响。
【渲染主线程】的工作模式
在了解了浏览器的进程模型后,我们得知了渲染进程
,是和我们前端开发者,最密切相关的一个进程栈,我们编写的全部代码,都需要通过渲染进程来执行,所以接下来我们就重点聊聊,在浏览器中,渲染主线程的工作方式。
渲染主线程
是浏览器中最繁忙的的线程,他需要处理的任务包括,但是不仅限于:
- 解析HTML
- 解析CSS
- 执行全局 JS 代码
- 处理图层
- …
针对要处理的诸多任务,渲染主线程面临的最大问题,就是如何去高效的调度这些任务,并进行的合理的资源分配?
如下面临的问题:
- 我正在执行⼀个 JS 函数,执行到⼀半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
- 当前正在执行⼀个 JS 函数,执行到⼀半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
- 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪⼀个呢?
- …
针对于这个问题,渲染主线程
想出了设计出了一个绝妙的处理方式:创建任务队列,按序排队执行
如下图例:
- 在渲染主线程启动的时候,主线程会进入到到一个
无限循环的
的执行模式中 - 在进行每一次循环时,都会检查消息队列池中当前是否有待执行的任务存在,如果有,则按顺序取出第一个去执行,执行完成后再次进入到下一次循环;如果任务池这时候没有待执行的任务时,主线程就会进入到休眠模式
- 其它的所有线程(包括其它进程的线程)可以随时的向
消息队列池
添加任务,新添加的任务会依次追加在任务池的末尾,在添加新任务的时候,如果这时主线程是休眠的状态,则会立即将其唤醒,继续执行循环拿取并执行任务。
扩展
:贴上一张Chrome 浏览器源码图例,如图34行开启了一个For 无限循环,对应了渲染主线程的循环执行。
结论:
这样一来,整套循环执行事务的流程,就被称之为
【事件循环】或消息循环
。
异步的概念:
在了解清楚了,浏览器的事件循环机制后,紧接着,就引出了 JS 的一个,新的知识点概念,叫做
异步函数
当我们的代码在执行的过程中,经常会遇到一些 无法立即执行的
任务,比如说:
- 计时器定点完成后才需要执行的任务:
setInterval
,setTimeout
- 需要在网络通信完成后执行的任务:
fetch
,XMLHttpRequest
- 需要在用户交互操作触发后再去执行的任务:
addEventListener
- …
如果当渲染主线程
在执行这些任务的时候,一直处于等待
的状态,等待这些任务的触发时机到达,那么就会导致渲染主线程长时间处于【阻塞】
的状态,从而可能会导致浏览器面临卡死崩溃
的处境
同步执行图例:
以上图例演示了,如果 如果当渲染主线程对所有的代码都采取同步化的执行方式
,那么在遇到这种非立即执行的代码时,主线程就只能按顺序依次执行,在这期间,主线程会造成严重的阻碍现象
后续的事情不能及时的响应执行,页面上会呈现出卡顿延迟的表现
结论:
渲染主线程承担着极其重要的工作,无论如何都不能阻塞!
对此:浏览器采用了 异步
执行的方式来解决这个问题。
异步
执行图例
使用异步的执行方式:
渲染主线程将永不阻塞
任务执行的优先级?
其实此处的标题,严格意义上,算是一个
伪命题
,因为严格意义上来说,任务是没有优先级的,所有的任务优先等级都是一样的。遵循着先进先出的规则执行
=>>>>>>但是:【消息队列池】是有优先级
划分的。
扩展补充:
在过去,对浏览器任务队列的理解是,浏览器只有两个任务队列:一个是普通的宏任务队列
,另一个是优先级高的:微任务队列
。但是如今这种模式,已经被一种新的模式取代了,因为如今随着浏览器场景的越来越复杂化,两种任务队列已经满足不了现在的需求了
。
如今根据 W3C 最新的解释是
: 点击查看W3C官方解释
- 抛弃了过去
宏任务
的说法 - 现在每一个任务都有一个
任务类型
,并且同一个类型的任务,必须排在同一个消息队列
,同时也提供了:不同类型的任务可以分属于不同的队列
,在每一次事件循环中,浏览器可以根据实际的情况,智能调度从不同的队列中取出任务执行(这一点,随着不同的浏览器厂商,甚至是,不同时期的版本,智能调度算法都不一样,了解即可。)
- 浏览器
必须提供一个【微队列池】
, 【微队列中的任务执行优先级要高于其他所有类型队列池中的任务
】。
附上:Chrome 浏览器源码片段图(部分):每一个字段都代表了一个任务类型。
在目前 Chrome 浏览器的实现中,至少包含了以下几种任务队列池:
- 微任务队列池:用于存放需要被立即执行的任务
优先级:【最高】
- 交互任务队列池:用于存放用户与页面交互后产生的事件处理任务
优先级:【高】
- 延时任务队列池:用于存放计时器时间到达后待执行的回调任务
优先级:【中】
- 网络任务队列池:用于存放网络资源请求和服务端交互处理的事务
优先级:【中】
- …等等
对于微任务队列
的理解,可以总结为:当渲染主线程开始拿取任务开始执行的时候,倘若这时候,有很多不同类型的任务池中,都存在有排队待执行的任务,渲染主线程,会优先去,执行微任务池中的任务,直到将微任务池清空后,才会去自由调度的执行其他类型任务池中的任务。
添加任务到微任务队列的主要使用方式是:Promise,MutationObserver
Promise.resolve().then(() => {
console.log("微任务优先级最高");
})
浏览器还有很多其它类型队列,由于和我们前端开发关系不大,所以不做过多阐述.
扩展小问答:
阐述一下 JS 的事件循环机制
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息队列中取出第⼀个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是⼀种更加灵活多变的处理方式。根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同⼀个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在⼀次事件循环中,由浏览器自行决定取哪⼀个队列的任务。但浏览器必须有⼀个微队列,微队列的任务⼀定具有最高的优先级,必须优先调度执行。
- 单线程是异步产生的原因
- 事件循环是异步的实现方式
JS 中的计时器能做到精确计时吗?为什么?
不行
因为:
- 计算机硬件没有原子钟,无法做到精确计时。
- 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差。
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差。
- 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
本章节 我们详细的概述分析了,基于JS为一种单线程语言的机制背后,其运行在浏览器环境中的时候,它的事件循环处理机制的逻辑,以及通过事件循环机制,也很好的理解了异步函数的概念,本章节内容不多,却是满满的干货,希望认真读完本章的小伙伴,对这一节的知识点能有新的理解。
🚵♂️ 博主座右铭:向阳而生,我还在路上!
——————————————————————————————
🚴博主想说:将持续性为社区输出自己的资源,同时也见证自己的进步!
——————————————————————————————
🤼♂️ 如果都看到这了,博主希望留下你的足迹!【📂收藏!👍点赞!✍️评论!】
——————————————————————————————