0.写在最开头
本文主要是阅读《how browsers work》这篇文章时作的学习笔记,同时也加入了自己的理解,从而将篇幅很长很长的原文“精炼”成了这篇文章,但还是推荐大家能耐住性子去看看原文,写的十分的好,虽然花时间比较多,但可以学到很多东西,绝对值得!!!
另外,如有写的不对的地方欢迎大佬评论区批评指正,那么让我们开始吧~
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盒子会被挤下去,其以基线对齐