浏览器组件
浏览器大体上由以下几个组件组成,各个浏览器可能有一点不同。
界面控件 – 包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分
浏览器引擎 – 查询与操作渲染引擎的接口
渲染引擎 – 负责显示请求的内容。比如请求到HTML, 它会负责解析HTML、CSS并将结果显示到窗口中
网络 – 用于网络请求, 如HTTP请求。它包括平台无关的接口和各平台独立的实现
UI后端 – 绘制基础元件,如组合框与窗口。它提供平台无关的接口,内部使用操作系统的相应实现
JS解释器 - 用于解析执行JavaScript代码
数据存储持久层 - 浏览器需要把所有数据存到硬盘上,如cookies。新的HTML5规范规定了一个完整(虽然轻量级)的浏览器中的数据库 web database
注意:chrome浏览器与其他浏览器不同,chrome使用多个渲染引擎实例,每个Tab页一个,即每个Tab都是一个独立进程。
Chrome浏览器使用多个进程来隔离不同的网页,在Chrome中打开一个网页相当于起了一个进程,每个tab网页都有由其独立的渲染引擎实例。因为如果非多进程的话,如果浏览器中的一个tab网页崩溃,将会导致其他被打开的网页应用。另外相对于线程,进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。
在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被冻结了.
JS为处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JS是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突;如果JS是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果,当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,JS在最初就选择了单线程执行。
GUI渲染线程与JS引擎线程互斥的,是由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致。当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。由于GUI渲染线程与JS执行线程是互斥的关系,当浏览器在执行JS程序的时候,GUI渲染线程会被保存在一个队列中,直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
浏览器定时计数器并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。
1.解析HTML生成DOM树 - 渲染引擎首先解析HTML文档,生成DOM树
2.构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
3.布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
4.绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来
以上步骤是一个渐进的过程,为了提高用户体验,渲染引擎试图尽可能快的把结果显示给最终用户。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。
上面确定了renderer的样式规则后,然后就是重要的显示元素布局了。当renderer构造出来并添加到Render树上之后,它并没有位置跟大小信息,为它确定这些信息的过程,接下来是布局(layout)。
浏览器进行页面布局基本过程是以浏览器可见区域为画布,左上角为 (0,0)基础坐标,从左到右,从上到下从DOM的根节点开始画,首先确定显示元素的大小跟位置,此过程是通过浏览器计算出来的,用户CSS中定义的量未必就是浏览器实际采用的量。如果显示元素有子元素得先去确定子元素的显示信息。
布局阶段输出的结果称为box盒模型(width,height,margin,padding,border,left,top,…),盒模型精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位。
在绘制(painting)阶段,渲染引擎会遍历Render树,并调用renderer的 paint() 方法,将renderer的内容显示在屏幕上。绘制工作是使用UI后端组件完成的。
回流(reflow):当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染。reflow 会从 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。
重绘(repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
每次Reflow,Repaint后浏览器还需要合并渲染层并输出到屏幕上。所有的这些都会是动画卡顿的原因。Reflow 的成本比 Repaint 的成本高得多的多。一个结点的 Reflow 很有可能导致子结点,甚至父点以及同级结点的 Reflow 。在一些高性能的电脑上也许还没什么,但是如果 Reflow 发生在手机上,那么这个过程是延慢加载和耗电的。可以在csstrigger上查找某个css属性会触发什么事件。
css会和html并行解析,但加载css时,css会阻塞渲染树得渲染,css加载会阻塞js语句得执行
遇到script标签,其会阻塞后面的html标签解析
注意只要浏览器遇到 script 标记,唤醒 JavaScript解析器,就会进行暂停 (blocked )浏览器解析HTML,并等到 CSSOM 构建完毕,才去执行js脚本
defer
defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行,无论此刻是 HTML 解析阶段还是
DOMContentLoaded 触发(HTML解析完成事件)之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script
可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
从上一段也能推出,多个 async-script 的执行顺序是不确定的,谁先加载完谁执行。值得注意的是,向 document 动态添加 script 标签时,async 属性默认是 true。