从浏览器运行机制解析到Event Loop

浏览器的进程与线程,浏览器内核,event loop

Posted by AzirKxs on 2022-07-11
Estimated Reading Time 10 Minutes
Words 3.1k In Total
Viewed Times

从浏览器运行机制解析到Event Loop

参考:

云中桥 《前端进阶」从多线程到Event Loop全面梳理》 链接:https://juejin.cn/post/6844903919789801486

null仔 《从 8 道面试题看浏览器渲染过程与性能优化》链接:https://juejin.cn/post/6844904040346681358

可乐好喝不胖 《css加载会造成阻塞吗?》链接:https://juejin.cn/post/6844903667733118983

dailc 《从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理》链接:https://juejin.cn/post/6844903553795014663

ps:我只是通过自己学习这几篇文章进行了一个总结,另外根据自己的理解进行了拓展,仅仅是一个学习笔记,感谢大佬们写的优质文章,建议去阅读原文

进程与线程

什么是进程?

CPU就是一座工厂,进程好比工厂的车间,一个工厂有多个车间。但是区别就是每次当一个车间工作的时候,别的车间就不能工作了。也就是说,CPU每次只能单独处理一个进程,而其他进程处于非运行状态。

CPU使用时间片轮转进度算法来实现同时运行多个进程。

什么是线程?

一个车间里,可以有很多工人,共享车间所有的资源,他们协同完成一个任务。

线程就好比车间里的工人,一个进程可以包括多个线程,多个线程共享进程资源。

cpu,进程与线程之间的关系

进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
不同进程之间也可以通信,不过代价较大
单线程与多线程,都是指在一个进程内的单和多

浏览器

对于计算机来说,每一个应用程序都是一个进程, 而每一个应用程序都会分别有很多的功能模块,这些功能模块实际上是通过子进程来实现的。 对于这种子进程的扩展方式,我们可以称这个应用程序是多进程的。

浏览器是多进程的,每一个标签页都是一个独立的进程。

浏览器所包含的四大进程

  • 主进程(Browser进程)
    • 协调控制其它子进程(页面的创建和销毁)
    • 浏览器界面的显示,用户的交互操作(前进,后退,收藏)
    • 将渲染进程得到的内存中的Bitmap,绘制到用户界面上
    • 处理不可见操作,网络请求,文件访问等
  • 第三方插件进程
    • 每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU进程
    • 用于3D绘制等
  • 渲染进程(浏览器内核,render进程)
    • 页面渲染,脚本执行,事件处理等

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

浏览器的渲染过程

1-1

  1. 解析html文件,构建DOM树,同时浏览器的主进程(Browser进程)负责下载CSS文件
  2. CSS文件下载完成,解析CSS文件成树形的数据结构,然后结合DOM树合并成RenderObject树
  3. 布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
  4. 绘制RenderObject树(paint),绘制页面的像素信息
  5. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面

浏览器内核(render进程)

ps:最初内核的概念包括渲染引擎与JS引擎,目前习惯直接称渲染引擎为内核,JS引擎独立。v8的js引擎就是独立的

进程与线程的关系为一对多,对于浏览器的渲染进程来说,也是多线程的,具体来说,包含以下的线程

  • GUI线程
    • 解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 页面需要重绘和回流时,该线程就会执行
    • 与js引擎线程互斥,防止渲染结果不可预期(当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执)行。
  • JS引擎线程
    • 负责处理解析和执行JavaScript脚本
    • 只有一个JS引擎线程
    • 与GUI渲染线程互斥,防止渲染结果不可预期
  • 事件触发线程
    • 用来控制事件循环
    • 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
  • 定时触发器线程
    • setInterval与setTimeout所在的线程
    • 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
    • 计时完毕后,通知事件触发线程
  • 异步http请求线程
    • 浏览器有一个单独的线程用于处理AJAX请求
    • 当请求完成时,若有回调函数,通知事件触发线程

Browser进程和浏览器内核(Renderer进程)的通信过程

  • Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程

  • Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染

    • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
    • 最后Render进程将结果传递给Browser进程
  • Browser进程接收到结果并将结果绘制出来

js引擎为什么是单线程的?

  1. 历史原因
    在创建 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。

  2. 多线程会引发一些问题

    • 因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。
    • 如果同时操作 DOM ,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期。如果 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。
    • 如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突。

为什么js会阻塞页面的加载?

这个问题也可以换一种问法,为什么JS引擎线程与GUI线程互斥?为什么js会阻塞dom的解析?

从底层来说,JavaScript是可以操控DOM和CSS的,如果在修改这些元素的同时渲染页面(同时运行JavaScript线程和GUI线程),那么渲染前后获得的元素数据可能就不一致了,因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。当JS引擎线程执行时GUI渲染线程会被挂起,GUI更新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行。

从运行上来说,在解析html文件的过程中(GUI),如果遇到了JS脚本,那么就会先执行JS脚本(JS引擎),执行完成之后,接着在解析。

浏览器接收到HTML文件后的动作:
1、HTML解析成DOM树
2、CSS解析成CSSOM树
3、若写了JavaScript代码块,且非async,则中断DOM构建,反过来执行JS,因为JS可能会修改DOM的结构内容

CSS的加载会不会造成阻塞?

根据浏览器渲染流程图,我们可以知道,DOM Tree和CSS Tree是并行构建的,所以CSS的加载不会阻塞DOM的解析,但是,Render Tree依赖DOM Tree和CSS Tree,因此CSS加载完毕后,才能开始渲染,因此CSS会阻塞DOM的渲染。此外,由于js可能会操作之前的Dom节点和css样式,因此浏览器会维持html中css和js的顺序。因此,样式表会在后面的js执行前先加载执行完毕,所以CSS会阻塞在其后面js的执行

由此衍生出了前端性能优化之关键路径渲染优化

DOMContentLoaded与onload的区别

  • 当 DOMContentLoaded 事件触发时,仅当 DOM 解析完成后,不包括样式表,图片。此外,css会阻塞js的执行,而js的执行会阻塞DOM的解析,因此我们需要把脚本放在末尾执行。

1、当页面里同时存在css和js,并且js在css后面的时候,DomContentLoaded必须等到css和js都加载完毕才触发。
2、其他情况下,DomContentLoaded不等待css加载,DomContentLoaded事件也不等待图片、视频等资源加载。

  • 当 onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片等资源已经加载完毕。

JS运行机制之Event Loop

1-2

JS 分为同步任务和异步任务
同步任务都在JS引擎线程上执行,形成一个执行栈
事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中
执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行

具体来说:
1-3

  • 当代码执行到setTimeout/setInterval时,实际上是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件,而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。
  • 当代码执行到XHR/fetch时,实际上是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件,而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。
  • 当我们的同步任务执行完,JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JS引擎线程执行

更新到ES6之后,有了微任务与宏任务的区分,再进一步个做划分:

  • 宏任务的任务队列是由事件触发线程维护
  • 微任务由JS引擎线程维护

执行过程:

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

此处还有一个小小的坑,那就是异步任务放入回调队列中的时机,并不是一遇到了异步任务就会把回调放入任务队列中的,异步处理结束后才会将回调放入事件队列中,具体是什么时机是根据web api指定的规则来决定的。

  • setTimeout:浏览器有个定时器(timer)模块,定时器到了执行时间才会把异步任务放到异步队列。
  • promise.then:promise 的状态发生改变才会把异步任务放到异步队列。

执行的过程与上一篇文章一样,这篇从线程的角度做了一个深入


如果这篇文章对你有帮助,可以bilibili关注一波 ~ !此外,如果你觉得本人的文章侵犯了你的著作权,请联系我删除~谢谢!