01 基本概念的确立
基本的查阅资料
- 浏览器的工作原理:新式网络浏览器幕后揭秘 这篇文章比较可信,或者说其实大部分人都是参考这篇文章讲的一些东西,但是为什么各有理解不同 => 有人甚至可能连同步和异步,进程和线程都没有弄明白,对一些基础知识认知片面,因此显得一知半解。
- 浏览器的线程和进程 @遇侎粒_duyuqin
- [译]官方图解:Chrome 快是有原因的,现代浏览器的多进程架构! 本文有一定篇幅直接复制的,但对很多存在疑惑的点,进行深度的解析,和一定程度的修改,甚至带上论证。
- 其它资料并没有太多的参考,觉得很多东西不够权威,没有参考价值,有些东西还是看书来的实在!
- 图解浏览器的基本工作原理
- 万字详文:深入理解浏览器原理 @腾讯技术部
一、浏览器是什么样的
(1) 从结构上看
- 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
- 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
- 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 网络- 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
- 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
- JavaScript 解释器。用于解析和执行 JavaScript 代码。
- 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
以上,说的更清楚一点,就是从服务(软件模块组成)去描述,一个浏览器需要这么多的服务。值得注意的是,这里的呈现引擎。
(2) 从进程,线程上看
-
浏览器主进程(Browser进程)
浏览器的主进程,负责协调、主控,只有一个(无论打开几个tab或几个弹窗),主要作用:
负责浏览器界面显示,与用户交互,如前进,后退等;
负责各个页面的管理,创建和销毁其他进程;
将Renderer进程得到的内存中的Bitmap,绘制到用户界面上;
网络资源的管理,下载等;
PS:看到有人说还有网络进程专门负责网络资源的加载,但是我从哪里都没找到证据论证,反而更多的人认为这个功能应该归属于浏览器的主进程 -
GPU进程
用于3D绘制等,可禁用掉,只有一个。 -
第三方插件进程
每种类型的插件对应一个进程,仅当使用该插件时才创建。 -
浏览器渲染进程
浏览器渲染进程(Renderer进程),即通常所说的浏览器内核进程,主要作用:页面渲染、脚本执行、事件处理等。每一个标签页的打开都会创建一个Renderer进程,且互不影响。默认一个标签页一个Renderer进程,但是,有时候浏览器会将多个进程合并(暂时没查到合并的依据)。它和结构上的呈现进程相对应。 下面是介绍它的线程。-
GUI渲染线程:
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等;
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行;
- 注意:GUI渲染线程与JS引擎线程是互斥的(什么是互斥,待会会讲到为什么),当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
-
JS引擎线程:
- JS引擎线程也称为JS内核线程,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,JS线程是当线程的 => 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序;
- 注意:GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
-
事件触发线程:
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS单线程自己都忙不过来,需要浏览器另开线程协助);
- 当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务队列添加到事件触发线程中(注意队列用词);
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎线程空闲的时候进行处理;
-
定时器触发线程:
- 即setInterval与setTimeout所在线程
- 浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确性
- 因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中
- 注意: W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms
-
异步http请求线程
- XMLHttpRequest连接后通过浏览器新开一个线程请求
- 检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行
总结:
-
(3) 解释一些让人疑惑的点
- 该节(1)中的图的箭头并不是包含关系,而是连接关系!这表明他们是这样通知其它组件工作交流,或者说的直白一点:当url一旦确立,要展示页面是,他们的组件是怎么样通知的。关于这个解释如何论证?看最右边的Data 存储服务,它是独立的!
- 呈现引擎所对应的线程就是GUI线程,对的你可能以为是渲染进程?那你可能猜错了!引用一段话
呈现引擎采用了单线程。几乎所有操作(除了网络操作)都是在单线程中进行的。在 Firefox 和 Safari 中,该线程就是浏览器的主线程。而在 Chrome 浏览器中,该线程是标签进程的主线程。
网络操作可由多个并行线程执行。并行连接数是有限的(通常为 2 至 6 个,以 Firefox 3 为例是 6 个)。
PS:这也说明了资源在理想情况下是能够(有能力)并行下载的!(如果什么都不考虑情况下;防止杆精)
二、一些问题的解释
(1) JS线程是单线程
本身单线程是不存在这个概念,单线程只是相对多线程而存在。或者说JS不是多线程,关于为什么JS不是多线程:
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?(这段话摘自《阮一峰的网络日志:JavaScript 运行机制详解:再谈Event Loop》)总结:
主要的一点,就是为了简单,没有DOM临界资源竞争,这也是为后面事件循环EventLoop的出现相互映衬
(2) 关于EventLoop
它其实在浏览器和NodeJs表现不一致。限于篇幅这里暂时不做过多解释。先挖个坑
(3) 浏览器渲染进程上面提到的各线程,并行?还是并发
- 假设它是并发,意味着,那些线程都是轮换的占用CPU:
- 定时器线程不可能轮换的占用,无法保证计时
- 现代的CPU大部分,几乎都是多核,为什么非要并发而不是并行,发挥多核的优势,因此并发没有必要
- 并发,意味着效率低下,前面也说了,JS线程和CUI渲染线程互斥,如果是并发,那就没有互斥的必要啊! (互斥,是有临界资源产生竞争)
- 假设它是并行,意味着,那些线程都是使用不同核的CPU:
- 定时器问题解决
- CPU利用率高,发挥多核优势
- JS引擎线程和GUI渲染线程互斥 的 前提 => 能并行
结论:浏览器渲染进程上面提到的各线程是并行的
(4) 为什么JS引擎线程和GUI渲染线程互斥
互斥,是有临界资源产生竞争。那这里临界资源是什么? DOM yes!
基于 (3),假设这两个线程不互斥,从一个时间点开始:
- JS线程改变某个DOM节点的一个高度,GUI渲染线程 渲染 DOM蓝本
- JS线程在 GUI渲染线程 渲染 DOM蓝本 完成之前,又删除 这个DOM节点,但是
GUI渲染线程不能倒退,它已经到了要渲染的时间,页面必须出现变化 - 于是,在JS线程的DOM蓝本是没有这个DOM节点,但是页面上依然显示,持续下去,整个页面和
JS内存中的DOM蓝本,表现的不同,差异越来越大。
上面解释了,为什么GUI渲染线程要和JS线程竞争
(5) 究竟什么是主线程
一个进程至少有一个线程,即主线程,它是进程的子线程的入口。
如果按照这种情况看:浏览器的渲染进程可以分为以下线程
- 主线程 Main thread,运行JavaScript、DOM、CSS、样式布局计算 (渲染线程GUI,JS线程)
- 工作线程 Worker thread
- N个工作线程:运行Web Worker,ServiceWorker,Worklet
- 内部线程:Blink和V8会创建几个线程处理web audio,数据库,GC等
- 排版线程 Compositor thread
- 光栅线程 Raster thread
- 定时器线程,等等,在进行线程介绍的时候有讲过
刚好JS线程和GUI线程本身就是互斥
综上:缩短JS线程的执行时间和提高执行效率对页面的展示有帮助。
(6) setTimeOut是什么时候将回调函数放入任务队列的?
答:在调用setTimeOut直到setTimeOut函数结束的那一段时间,如果简单一点理解(模糊),在JS线程的JavaScript引擎在解析setTimeOut函数时,调用底层接口通知定时器线程开始计时。定时器线程在计时完成,将回调函数入队任务队列,等待JS线程执行。
setTimeout(()=>{
console.log('a')
}, 2000)
for(let i=0; i<5000000000; i++) {
1-1 > 2
}
console.log('结束了')
这段代码说明setTimeout是在调用了,就开始计时,等到计时完成,直接插入任务队列,等待主线程执行该任务,但是主线程当前的JS代码for循环还未结束,因此等到for循环结束,打印“结束了”,再执行定时器任务前的任务,执行轮询到该定时器任务才打印"a"