浏览器渲染原理
# 0.写在最开头
本文主要是阅读《how browsers work》 (opens new window)这篇文章时作的学习笔记,同时也加入了自己的理解,从而将篇幅很长很长的原文“精炼”成了这篇文章,但还是推荐大家能耐住性子去看看原文,写的十分的好,虽然花时间比较多,但可以学到很多东西,绝对值得!!!
另外,如有写的不对的地方欢迎大佬评论区批评指正,那么让我们开始吧~
# 1.浏览器构成
浏览器构成的主要组件有:用户界面、浏览器引擎、渲染引擎
、网络、UI 后端、JS 解释器、数据存储
# 2.Rendering engine
渲染引擎(rendering engine)
主要将请求到的文件内容渲染成为页面,不同浏览器渲染引擎不同:
Firefox:
Gecko
Safari:
WebKit
(开源)Chrome:
Blink
(WebKit 的一个分支)IE:Trident
另外,渲染进程是多线程
的,html、css 解析、js 脚本执行、重排重绘、事件循环都在这个进程中执行
# 3.Render engine 解析流程
这里主要介绍Webkit引擎
,下面这个渲染流程图建议牢牢记好,全文将会围绕这个展开:
- HTML 解析器将 html 文件解析成
DOM Tree
,CSS 解析器将 css 文件解析成Style Rules
- 将 DOM Tree 和 Style Rules 进行
Attachment
(连结)生成Render Tree
Render Tree
由多个带有视觉属性(尺寸、样式)的矩形构成,需要逐个计算大小、位置,然后Layout
(即布局,重新 Layout 即重排
)- 最后
Painting
,绘制页面完成后展示
Gecko引擎
渲染流程如下:
对比 WebKit 基本流程是一样的,只是一些术语不同:
Frame Tree
== Render Tree
Reflow
== Layout
(回流和重排是一个意思)
Content Model
== DOM Tree
Frame Constructor
== Attachment
# 4.Parsing
解析
是渲染引擎中一个重要的工作,可以将文档结构转化为代码可以使用的结构。(注意:这里介绍的是引擎通用的解析过程而非只针对于渲染引擎)
通用解析分为两个过程:词法分析(Lexical)
和语法分析(Syntax)
。首先词法分析
将文档内容转化为可识别标志,之后通过语法分析
构建解析树(Parse Tree)
,流程如下:
解析的最终结果会得到Parse Tree
,然后会通过编译
转化成机器能识别的机器码
# 5.HTML 解析器
渲染引擎里的 HTML 解析器可将 HTML 文档解析为解析树(对 HTML 来说得到的解析树就是DOM Tree
),其中遵循的词法分析
和语法分析
规范由 W3C 制定。
HTML 结构如下:
<html>
<body>
<p>Hello World</p>
<div><img src="example.png" /></div>
</body>
</html>
解析成 DOM Tree 结构如下:
# 5.1 解析算法
HTML 解析算法包括两个阶段:标记化(tokenization)
和树构建(tree construction)
标记化对应解析过程中的
词法分析
,标记器(tokenizer)
根据词法规范
会将 HTML 代码解析为一个个标记(tokens)
,包括开始标记
、结束标记
、属性名称
、属性值
。之后标记器每解析出一个标记就会交给树构建器
,然后又开始准备下一轮解析树构建对应解析过程中的
语法分析
,接收来自标记器的一个个标记,将其解析为一个个DOM
,根据语法规范
动态插入,最终构建生成DOM Tree
# 6.CSS 解析器
CSS 解析器与 HTML 解析过程类似,通过词法分析
识别css选择器标识符
、样式属性标识符
以及样式属性值
等,再通过语法分析
阶段解析得到Style Rules
即解析的最终结果Parse Tree
拿 Webkit 引擎里的 CSS 解析器作说明:它会将每个 css 文件解析为一个StyleSheet(样式表)
对象,内部由一个个CSSRule(css规则)
对象构成,而CSSRule
内部由两个对象Selections(选择器)
和Declaration(声明)
构成
例如这一段 css 代码
p,
div {
margin-top: 3px;
}
.error {
color: red;
}
最终解析得到的Style Rules
树为:
# 7.Render Tree 的构建
# 7.1 Render Tree 构建过程
经过 HTML 解析和 CSS 解析生成了DOM Tree
和Style RUles
树之后,将两者Attach
最终会构建生成Render Tree
,如下图所示
Render Tree
里的每个节点为RenderObject
,其外在表现为一个矩形框,几何信息包含宽高、位置、样式、z-index 等。
RenderObject
分为很多种类型,由display
属性决定,从创建RenderObject
的 webkit 代码可以看出
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) { // 判断其display类型
case NONE: // display:none,不会创建renderObject,意味着将该节点不会插入到文档中
break;
/**
* 题外话:回答display:none和visibility:hidden的区别时可以扯一下这个
* 在display为none时,元素在构建渲染树这一环节已经被gank掉了(不会生成renderObject),
* 不会参与后续的layout和paint环节
* 而visibility:hidden,只是将元素设置为视觉不可见,
* 还是会生成renderObject并参与后续的layout和paint环节)
*/
case INLINE: // inline
o = new (arena) RenderInline(node);
break;
case BLOCK: // block
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK: // inline-block
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM: // list-item
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
# 7.2 Render Tree 和 DOM Tree 的区别
Render Tree 与 DOM Tree 在结构上并不是一 一对应的:
1.以下 HTML 元素不会被插入到 Render Tree 中
header
meta
title
display:none
... ...
2.一个 renderObject 对应一个 DOM 节点,但若该节点的开启了
float
、absolute
、fixed
等时,会被放置在 Render Tree 的不同位置,不一定是按照 DOM Tree 文档流排列(这就是所谓的脱离文档流
)
# 8.Layout
# 8.1 Layout 概述
在renderObject
被添加到Render Tree
时会计算其位置和大小,这个过程就叫布局(Layout)
,在 gecko 引擎称之为回流(reflow)
HTML 使用流式布局
,即由左到右,由上到下
进行布局,这样的布局有个特点是后进入流中的元素不会影响先进入流中的元素;坐标系基于根元素,零点位于左上角
布局是一个递归过程,从根节点开始逐层递归调用renderObject.layout()
计算每个节点的位置和大小信息。layout 方法定义在每个renderObject
里,webkit 代码如下:
class RenderObject{
virtual void layout(); // 布局
virtual void paint(PaintInfo); // 绘制
virtual void rect repaintRect(); // 重绘重排组合
Node* node; // DOM节点
RenderStyle* style; // 计算样式
RenderLayer* containgLayer; //the containing z-index layer
}
# 8.2 Dirty bit 系统
Dirty bit
即脏位系统
。在后期修改了一个节点的位置和大小会重新触发 layout,这种过程叫做重排
。
为了性能考虑,对于局部改变只需局部重排而非整体重排,故渲染引擎引入了Dirty bit system
,在需要重排的renderObject
及其子元素上标记dirty
字段,随后在重排开始时遍历标记为dirty
的renderObject
,调用其layout
方法触发重排
# 8.3 Layout 过程
- 父 renderObject 计算其宽高、位置
- 遍历子 renderObject,将其在放置于自身容器里,若子 renderObject 的 dirty 标志为 true,调用其 layout 方法重新计算其宽高、位置
- 用子级盒子宽高来填充自身宽高
- 设置 dirty 标记为 false,表示已经 layout 好了
# 8.4 异步、同步、局部、全局重排
异步重排
:为了避免频繁的重排,通常会采用一个异步的方式,即将多个需要重排的工作先放入一个队列中,待队列满了或者最小时间间隔到了,才会统一触发重排
同步重排
:同步意味着立刻重排,修改 DOM 的以下属性会触发
offset
:offsetTop、offsetLeft、offsetHeight、offsetWidthscroll
:scrollTop、scrollLeft、scrollHeight、scrollWidthclient
:clientTop、clientLeft、clientHeight、clientWidth- ... ...
局部重排
:只在局部进行重排,修改 DOM 的大小和位置或者添加、删除、替换 DOM 等操作会影响局部的布局,这些会触发局部重排,修改以下属性会触发:
- width,height,
- margin,padding,
- position
- display:none
- ... ...
全局重排
:以下情况会触发
- 网页初始化时
- 全局样式更改,例如字体大小
- 屏幕大小调整
- ... ...
# 8.5 重排优化
重排是一个非常耗性能的工作,应尽量避免,有很多情况可以优化
- 修改多个样式时给 DOM 添加 class 名,设置 class 的样式一次性修改
- 先把 DOM 的 display 设置 none,修改完后再显示
- 向一个父节点添加多个子节点时,先创建
documentFragment
,将子节点添加到其中,最后再把其一次性插入到父节点 - 使用动画的元素会频繁触发重排,为其开启 fixed 或 absolut 使其脱离文档流即可,或者可以的话使用 gif 图代替
- 不要使用 table 布局
- ... ...
# 9.Paint
renderObj
经过 Layout 阶段布局完成后,会调用renderObj.paint()
开始绘制节点样式
# 9.1 局部绘制
当某个节点发生改变时,其对应的renderObj
会使其在屏幕上的矩形框失效,这就会让操作系统判断其为“脏区”
并进行重绘(repaint)
# 9.2 绘制顺序
绘制是按照元素的样式堆叠顺序进行的,一个块元素的绘制顺序为:
background-color
background-image
border
children
outline
# 9.3 重排和重绘的关系
一句话:"重绘不一定重排,重排一定重绘"
如图中所示,改变一个 DOM 的大小、位置或者向一个 DOM 节点进行增删改都会有可能触发 layout 重排,之后一定会 repaint 重绘,但是只修改 DOM 的某些样式,不影响其大小、位置那么就不会触发重排,只用重绘就行。
# 10.CSS2 视觉模型
# 10.1 css 盒模型
CSS 盒模型将一个元素看作是一个矩形框,框的宽高从外到内由其margin
、boder
、padding
、content
构成,如下图:
其中,css 属性box-sizing
决定了width
和height
要作用于哪个区域,,默认作用于content区
即box-sizing:content-box
,还有两个可选值padding-box
和border-box
# 10.2 定位方案
1.定位的方案有三种:
normal
:这是默认的定位方案,根据元素框展示类型
即(display属性
)和尺寸来布局
float
:首先像正常文档流布局,然后脱离文档流,尽可能向左或向右浮动
absolute
:脱离文档流,按照其他方式布局
2.盒子的布局方式由这几个因素决定:
展示类型即display
盒子尺寸
定位方案
屏幕大小
等外部因素
# 10.3 盒子展示类型
通过display
设置盒子的展示类型
block
:会形成一个块,在浏览器窗口有一个自己的矩形,且块是在垂直方向一个接一个放置的
inline
:没有自己的块,会被包含在块内朝水平方向一个接一个放置
当父元素 content 区域宽度不够时,inline 盒子会被挤下去,其以基线对齐