真正落实到把每一个点讲清楚!!!

大家好,本人是 我是哪吒,个人Github账号 webVueBlog ,系列文章更多来源,欢迎 Follow

本文章的特点,通俗易懂,比什么浏览器原理的某些文章更加从小白的点,一步步带入理解,从一个点慢慢扩散到体系闭环。讲到的点,词,都是有关联的哦,注意一下哦!

1. 我们都知道JS是单线程的,那么什么是进程,线程呢?

CPU是计算机的核心,它承担了所有的计算任务,就像是一座工厂,时刻在运行。假定工厂的电力是有限的,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

那么进程就好比工厂的车间,它代表CPU所能处理的单个任务。(进程是CPU资源分配的最小单位,是能拥有资源和独立运行的最小单位;字面意思就是进行中的程序,将它理解为一个可以独立运行且拥有自己的资源空间的任务程序,进程包括运行中的程序和程序所使用到的内存和系统资源)进程之间相互独立,任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

但进程相对于来说就是一个应用软件启动或者打开一个浏览器tab标签,运行多个进程,比如你打开编辑器敲代码的同时可以打开听歌软件。

也就是说CPU可以有很多进程,我们的电脑每打开一个软件就会产生一个或多个进程,为什么电脑运行的软件多就会卡,是因为CPU给每个进程分配资源空间,但是一个CPU一共就那么多资源,分出去越多,越卡,每个进程之间是相互独立的,CPU在运行一个进程时,其他的进行处于非运行状态,是CPU使用时间片轮转进度算法 来实现同时运行多个进程。

拿Chrome来说,我们每打开一个Tab页就会产生一个进程,我们使用Chrome打开很多标签页不关,电脑会越来越卡,不说其他,首先就很耗CPU

线程好比一个车间里,可以有一个工人,或者可以有很多工人,共享车间所有的资源,他们协同完成一个任务。即线程好比车间里的工人,一个进程可以包括一个线程或者多个线程,多个线程共享进程资源(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。 (线程是CPU调度的最小单位,线程是建立在进程的基础上的一次程序运行单位,通俗来说,线程就是程序中的一个执行流,一个进程可以有多个线程)

一个进程中只有一个执行流称为单线程,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。

一个进程中有多个执行流称为多线程,即在一个程序中可以同时运行多个不同的程序来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

单线程与多线程,都是指在一个进程内的单和多。不同进程之间也可以通信,不过代价很大。- 调度和切换:线程上下文切换比进程上下文切换要快得多

我们常常接触浏览器,而浏览器就是多进程的,每一个tab页面,就是一个独立的进程。对于计算机来说,每一个应用程序都是一个进程,而每一个应用程序都会分别有很多的功能模块,这些功能模块实际上是通过子进程来实现的。

对于这种子进程的扩展方式,我们可以称为这个应用程序是多进程的。对于浏览器来说,浏览器就是多进程的,在Chrome浏览器中打开了多个tab页面,然后打开windows控制管理器,我们可以看到一个Chrome浏览器启动了好多个进程。那么你需要了解浏览器有哪些主要的进程呢?

2. 那么浏览器包含了哪些进程呢?

一般是不是很难记住呢,不急,接下来我慢慢讲,不用死记的其实,好比你知道1+1=2,通过阅读下面的内容就正常的知道了呢?

浏览器主要的进程,从浏览器是多进程讲打开浏览器启动了哪些进程:

i: 主进程(Browser进程,该进程只有一个),打开后的主进程(主要的嘛): 浏览器的主进程负责协调,控制其他子进程 tab(负责各个页面的管理,创建和销毁其他进程);浏览器界面显示,用户交互,前进,后退,收藏;将渲染(Renderer)进程得到的内存中的Bitmap(位图),绘制到用户界面上;处理一些不可见的操作,比如网络资源的管理(网络请求),下载,文件访问等。

ii: 第三方插件进程: 就是每种类型的插件对应一个进程,仅当使用该插件时才创建GPU进程

iii: GPU进程: 该进程也只有一个,用于3D绘制等等

iiiI: 渲染进程,即通常所说的浏览器内核(Renderer进程,内部是多线程): 负责页面渲染,脚本执行,事件处理等;每个Tab页面都有一个渲染进程,互不影响。浏览器内核(渲染进程),对于渲染进程来说,它当然也是多线程的了

记住:主进程,第三方插件进程,GPU进程,渲染进程等,这些后面会构成一个体系联系到的。那么我们常说的渲染进程,需要了解哪些线程呢,让你了解如何在浏览器显示页面打下基础。

3. 渲染进程包含哪些线程?

上面讲到渲染进程,那么渲染进程里有哪些线程在服务,运行程序呢?

i: GUI渲染线程: 负责渲染页面,布局和绘制;页面需要重绘和回流时,该线程就会执行;与js引擎线程互斥,防止渲染结果不可预期。

详细描述下: 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等;解析HTML代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree;解析CSS,生成CSSOM(CSS规则树);把DOM Tree和CSSOM结合,生成Rendering Tree(渲染树);当我们修改了一些元素的颜色或者背景色,页面就会重绘(Repaint);当我们修改元素的尺寸,页面就会回流(Reflow);当页面需要Repainting和Reflow时,GUI线程执行,绘制页面;回流(Reflow)比重绘(Repaint)的成本要高,我们要尽量避免Reflow和Repaint;GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时,GUI线程会被挂起(想当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

ii: JS引擎线程: 负责处理解析和执行JavaScript脚本程序;只有一个JS引擎线程(单线程);与GUI渲染线程互斥,防止渲染结果不可预期。

详细描述下: JS引擎线程就是JS内核,负责处理JavaScript脚本程序(例如V8引擎),JS引擎线程负责解析JavaScript脚本,允许代码;JS引擎一直等待任务队列中任务的到来,然后加以处理;浏览器同时只能有一个JS引擎线程在运行JS程序,所以JS是单线程运行的;一个Tab页面(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序;GUI渲染线程与JS引擎线程是互斥的,JS引擎线程会阻塞GUI渲染线程,就是我们常遇到的JS执行时间过长,造成页面的渲染不连贯,导致页面渲染加载阻塞(就是加载慢)。

例如浏览器渲染的时候遇到 script 标签,就会停止GUI的渲染,然后JS引擎线程开始工作,执行里面的JS代码,等JS执行完毕,JS引擎线程停止工作,GUI继续渲染下面的内容。所以如果JS执行时间太长就会造成页面卡顿的情况。

iii: 事件触发线程: 用来控制事件循环(鼠标点击,setTimeout,ajax等);当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中

详细描述下: 属于浏览器而不是JS引擎,用来控制事件循环,并且管理着一个事件队列(task queue);当JS执行碰到事件绑定和一些异步操作(如setTimeout,也可来自浏览器内核的其他线程,如鼠标点击,AJAX异步请求等),会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待JS引擎线程空闲时来处理。

当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。因为JS是单线程,所以这些待处理队列中的事件都得排队等待JS引擎处理。

iiii: 定时触发器线程: setTnterval与setTimeout所在的线程;定时任务并不是由JS引擎计时的,是由定时触发线程来计时的,计时完毕后,通知事件触发线程。

详细描述下: setInterval与setTimeout所在线程,浏览器定时计数器并不是由JavaScript引擎计数得(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响计数器的准确);通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待JS引擎空闲后执行),这个线程就是定时触发器线程,也叫定时器线程

W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms

iiiii: 异步http请求线程: 浏览器有一个单独的线程用于处理AJAX请求,即用于异步http请求,当请求完成时,若有回调函数,通知事件触发线程。

详细描述下: 在XMLHttpRequest连接后是通过浏览器新开一个线程请求,在检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中再由JavaScript引擎执行。

简单地说,就是当执行到一个http异步请求时,就把异步请求事件添加到异步http请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待JS引擎线程来执行。

我画个图如下:

看完图后,我再次详细描述一下啊,首先要知道,JS分为同步任务和异步任务,由图去讲解一下事件循环呢?

4. 事件循环(Event Loop)初探,深入了解一下?

那么同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会形成一个执行栈,主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放入一个事件回调。

一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行。

浏览器上的所有线程的工作都很单一且独立,非常符合单一原则

之前说过线程,那么结合说下你会更加清楚明白,定时触发线程,它只管理定时器且只关注定时不关心结果,定时结束后就把回调扔给事件触发线程。异步http请求线程只管理http请求同样不关心结果,请求结束把回调扔给事件触发线程。事件触发线程只关心异步回调进入事件队列,而我们JS引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这样反反复复就是我们所说的事件循环(Event Loop)

说到事件循环,就要说说其中的宏任务(macrotask)& 微任务(microtask),我讲解完这个估计,异步出的题,不再难事了哦

5. 那么什么时宏任务和微任务呢?

宏任务(macrotask)在ECMAScript中,macrotask 也被称为 task。

我们可以将每次执行栈执行的代码当作是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行),每一个宏任务会从头到尾执行完毕,不会执行其他。

由上面我们可以知道,由于JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。

宏任务 -> GUI渲染 -> 宏任务 -> …

常见的宏任务: 主代码块 script ;setTimeout;setInterval;setImmediate() - Node ;requestAnimationFrame() - 浏览器

微任务(microtask)

ES6新引入的Promise标准,同时浏览器实现上多了一个microtask微任务概念,在ECMAScript中,microtask也被称为jobs。我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务,而微任务可以理解成在当前宏任务执行后立即执行的任务。

当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完

宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> …

常见的微任务:Promise,process.nextTick() - Node ;Promise.then() ;catch ;finally;Object.observe;MutationObserver

先答疑:学到这里,很多问题可以问,如为什么JavaScript是单线程的呢?为什么GUI渲染线程与JS引擎线程互斥呢?

6. 答疑?

为什么JavaScript是单线程的?

JavaScript是单线程是历史原因,在创建JavaScript这门语言时,多进程多线程的架构并不流行,硬件支持并不好;因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高;如果同时操作DOM,在多线程不加锁的情况下,最终会导致DOM渲染的结果不可预期。

JS的单线程,与它的用途有关。作为浏览器的脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则带来很多复杂的同步问题。

JS还有Worker线程,为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程是完全受主线程控制的,而且不得操作DOM,所以,这个标准并没有改变JavaScript是单线程的本质。

为什么GUI渲染线程与JS引擎线程互斥呢?

JS是可以操作DOM的,如果同时修改元素属性并同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素就可能不一致了。为了防止渲染出现不可预期的结果,浏览器设定GUI渲染线程和JS引擎线程为互斥关系,当JS引擎线程执行时GUI渲染线程会被挂起,GUI更新则被保存在一个队列中等待JS引擎线程空闲时立即被执行。

setTimeout/setInterval和XHR/fetch代码,这些代码执行时,本身是同步任务,而其中的回调函数才是异步任务。

当代码执行到setTimeout/setInterval时,实际上是JS引擎线程通知定时触发线程,间隔一个时间后,会触发一个回调事件,而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。

当代码执行到XHR/fetch时,实际上是JS引擎线程通知异步http请求线程,发送一个网络请求,并指定请求完成后的回调事件,而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。

从进程、线程的角度来解释单线程的JS为什么拥有异步的能力,JS是单线程的,JS是通过事件队列(Event Loop)的方式来实现异步回调的。 上面也说过,但还是要记住:JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。

总结:

执行一个宏任务(栈中没有就从事件队列中获取),执行过程中如果遇到微任务,就将它添加到微任务队列中,宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行),当前宏任务执行完毕,微任务执行完毕,开始检查渲染,然后GUI线程接管渲染完毕后,JS线程继续接管,开始下一个宏任务。

做题:

页面的背景色直接变成黑色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了黑色,然后才执行的渲染。

那么浏览器为什么是多进程呢?

我们假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差;同理如果插件崩溃了也会影响整个浏览器

浏览器进程有很多,每个进程又有很多线程,都会占用内存这也意味着内存等资源消耗会很大,有点拿空间换时间的意思,简述渲染进程Renderer,页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程。(渲染进程是多线程的)

7. 浏览器执行事件循环过程是如何的呢?

浏览器会先执行一个宏任务,紧接着执行当前执行栈产生的微任务,再进行渲染,然后再执行下一个宏任务。

微任务和宏任务不在一个任务队列,例如setTimeout是一个宏任务,它的事件回调在宏任务队列;Promise.then()是一个微任务,它的事件回调在微任务队列,二者并不是一个任务队列。

以Chrome为例,有关渲染的都是在渲染进程中执行,渲染进程中的任务(DOM树构建,JS解析…等等)需要主线程执行的任务都会在主线程中执行,而浏览器维护了一套事件循环机制。

主线程上的任务都会放到消息队列中执行,主线程会循环消息队列,并从头部取出任务进行执行,如果执行过程中产生其他任务需要主线程执行的,渲染进程中的其他线程会把该任务塞入到消息队列的尾部,消息队列中的任务都是宏任务。

微任务是如何产生的呢? 当执行到 script 脚本的时候,JS引擎会为全局创建一个执行上下文,在该执行上下文中维护了一个微任务队列,当遇到微任务,就会把微任务回调放在事件触发线程队管理的事件队列中(微队列),当所有的JS代码执行完毕,在退出全局上下文之前引擎会去检测该队列,有回调就执行,没有就退出执行上下文。

注:有的同学认为微任务要早于宏任务,但想一下,一开始进入是不是 script 的宏任务呢?

  1. 微任务练习题:关于Promise

可以理解为,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then 的异步。

本篇内容:从进程,线程去了解浏览器内部的流程原理,通俗易懂,如果喜欢,请给大大地支持,关注,分享转发,您的支持是我最大的动力。

@Github: webVueBlog 

未经许可|不得在任何平台转载

Copyright@2022 我是哪吒|All Rights Reserved