这里写自定义目录标题
- 写在前面的话
- 浏览器是如何渲染页面的?
- 1、解析HTML ( Parse HTML)
- 2、样式计算( Recalculate Style)
- 3、布局( Layout)
- 4、分层( Layer)
- 5、绘制(Paint)
- 6、分块(Tiling)
- 7、栅格化(Raster)
- 8、画(Draw)
写在前面的话
最近在看袁老师(渡一教育)讲的前端大师课,觉得讲解的非常通俗易懂,也根据最新官方文档做了知识更新,摒弃了已过时的内容。拿到课件后搜了一下,各大网站上已经有很多转载课件内容的,感觉都是照搬课件,没有加上自己的理解,但是也没注明文章来源,所以本文觉得还是有必要再分享一次,希望大家看了有所收获。
前几年有个很火的面试题,问在浏览器地址栏输入URL,按下回车后究竟发生了什么?
从大方向上说应该分为两步,第一步是通过通信请求拿到html文档,这个文档包含了Dom、css和js,第二部就是浏览器把这个html文档的内容通过一系列复杂的操作渲染显示出来。这两个步骤背后都有非常复杂又巧妙的实现,今天重点探讨第二步具体实现过程。
浏览器是如何渲染页面的?
(建议理解记忆)
当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
整个渲染流程分为多个阶段,分别是: HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画。每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。
这样,整个渲染流程就形成了一套组织严密的生产流水线。
1、解析HTML ( Parse HTML)
解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。
1. HTML 解析过程中遇到 CSS 代码怎么办?
为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。
如果主线程解析到link
位置(外部样式表),此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是== CSS 不会阻塞 HTML 解析的根本原因==。
2. HTML 解析过程中遇到 JS代码怎么办?
如果主线程解析到script
位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。
第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
3. 为什么要构建DOM树?
这是因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构-DOM树。在控制台打印typeof document
显示object,可以打印console.dir(document)查看DOM树对象信息。
CSSOM 树也是同理,根节点是StyleSheetList,第二层是引入的外部样式表、内部样式表、内联样式表及浏览器默认样式表等。通过js可以操作DOM元素的样式(dom.style)。在控制台打印document.styleSheets
可以打印StyleSheetList数组信息,包含了当前页面的所有内部和外部样式信息。可以修改这个数组来改变全局的样式,比如给所有的div样式加个红色边框,命令是document.styleSheets[0].addRule('div', 'border: 2px solid #f40 !important')
2、样式计算( Recalculate Style)
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。在这一过程中,很多预设值会变成绝对值,比如red
会变成rgb(255,0,0)
;相对单位会变成绝对单位,比如em
会变成px
这一步完成后,会得到一棵带有样式的 DOM 树。
3、布局( Layout)
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置,布局完成后会得到布局树。
注意:大部分时候,DOM 树和布局树并非一一对应。
比如display:none
的dom元素没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器(:before),虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒(新的w3c标准中不再提行级元素、块级元素了,改为行盒和块盒)等等都会导致 DOM 树和布局树无法一一对应,而且布局树中的对象也不是dom对象,是处理过的其他类型对象。
什么是包含块?
就是元素的尺寸和位置,会受它的包含块所影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值(比如 position 被设置为 absolute 或 fixed),当我们对其赋予百分比值时,这些值的计算值,就是通过元素的包含块计算得来。
4、分层( Layer)
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。浏览器页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。
我们可以通过开发者工具中的图层来查看当前页面的分层,可以移动或者旋转查看当前页的分层,见下图:
主线程会使用一套复杂的策略对整个布局树中进行分层。分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change
属性更大程度的影响分层结果(设置哪个元素的哪个属性会发生改变,浏览器会对设置的元素单独分层)。
5、绘制(Paint)
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
6、分块(Tiling)
Tiling 英 [ˈtaɪlɪŋ] n.(统称)瓦,瓷砖;
绘制完成后、然后会主线程会将每个图层的绘制信息提交给合成线程。合成线程也在渲染进程中,会对图层进行分块、分成许多很小的块。合成线程会从线程池中拿取多个线程来完成分块工作。
7、栅格化(Raster)
栅格化是将每个块变成位图,优先处理靠近视⼝区域的块。图块是栅格化执行的最小单位。栅格化的本质是坐标变换、几何离散化、然后再填充。
合成线程会将块信息交给 GPU 进程,GPU 进程会开启多个线程以极高的速度完成栅格化。
8、画(Draw)
最后一个阶段就是画了
合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)[kwɒd]直译是四边形」信息。
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。变形发生在合成线程,与渲染主线程无关,这就是transform
效率高的本质原因。
合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。
以上就是浏览器渲染页面的整个过程,其中可能遇到属性改动重新计算layout树,后面的步骤重新执行、重新绘制等,了解到整个过程就可以了,具体哪个环节变动了怎么更新后面的环节应该大多数人用不到,感兴趣的自己看浏览器源码吧,chrome浏览器的源码是开源的,可以git上搜索:chromium