浏览器大体由七部分组成:
- 用户界面:包括地址栏、前后退按钮、书签菜单等;除了主窗口用以显示请求的界面外,其余都属于用户界面;
- 浏览器引擎:在用户界面和渲染引擎间传递指令;
- 呈现引擎:负责将请求来的数据呈现出来;
- 网络:用于网络请求;
- JS 解释器:用于解析和执行 JS 代码;
- UI 后端:用以绘制一些窗口小组件,比如弹窗等;
- 数据存储:存储一些浏览器所需数据,比如 cookie;
这其中,最重要的是呈现引擎,它负责解析传过来的 HTML 和 CSS 文件,并将其转化成可视化的界面。
呈现引擎的大体工作过程如下:
- 解析 HTML 和 CSS 文件,分别生成 DOM 树 和 CSSOM 树;
- DOM 树 和 CSSOM 树合并成为 Render 树;
- 布局;
- 绘制(渲染);
在从 1 到 2 的过程中,还穿插着 JS 脚本的执行,有一些 incline css 也需要汇到 CSSOM 上:
** HTML 解析**
- 先将 HTML 代码 token 化;
- 建构解析树;
- 将解析树转化为 DOM 树;
在构建 DOM 树时,如果遇到脚本执行,DOM 树就要暂停构建,执行脚本,执行完毕后重新 token 化。为了防止这种情况出现,一般将 script 放在最后或者用 defer 或异步的方式执行。
这里解释一下什么叫 token 化,其实就是将 html 以一种语义化的方式表达出来,这样浏览器才知道怎么建构解析树;
当 HTML 被语义化后,解析树就出来了:
继而再根据解析树构建 DOM 树:
CSS 解析
解析器会将 CSS 文件解析成 StyleSheet 对象,每个对象对应一个 CSS 规则。CSS 规则包含选择器和声明对象。
Render 树
Render 树,就是以 DOM 为主体,把 CSS 里的规则加到 DOM 树上(这里涉及到同一节点样式的计算,内联样式,外部 CSS 文件,浏览器默认规则等等)。
有一些隐性的属性,比如 head 、script ,title 或是 CSS 中设置了隐藏的元素不显示。
下图是 DOM 树(左)和 Render 树(右)的对比:
这里要说明的是,浏览器渲染不是按照 Render 树来的,为了方便处理,它会把 Render 树转化成 Layer 树。
Render 树上的 node 又称为 RenderObject,根据某些规则(是否有页面根节点等,细节不纠),一些 RenderObject 会生成对应的 RenderLayer,而没有被选中的,则从属于父类的 RenderLayer。
说白了,浏览器不是一个平面,它是一个立体面,得先决定哪些先画,哪些后画。
Layer 决定了页面的层次结构,RenderObject 决定了我们看到的内容。
布局
布局是一个递归的过程,要计算出每个节点在屏幕上的实际位置和尺寸,从父节点开始,到子节点。
布局的元素是以盒模型呈现,一般按从上到下,从左到右的方式进行。
布局的过程中会出现重排的问题,比如字体样式改变或屏幕大小改变,那就要重新布局。对于一些微小的变化,一采用 dirty 位的方式,需要改变的元素将自己标注为 dirty,布局时只需要重新计算该区域的信息即可。
绘制
绘制工作是 UI backend 做的,一般的绘制顺序如下:
- 背景颜色;
- 背景图片;
- 边框;
- 子代;
- 轮廓;
重绘时,浏览器会将原来的页面保存起来,只绘制两者间有差异的地方,而不会重绘整个页面。
这还没完,实际上,在 Render 树 变成 Layer 树时,还有另一进程发生了,有一些Layer 会变成 GraphicsLayer,交给 GPU 去处理(之前的都是 CPU)。好处是 GPU 更擅长处理某些图形绘制工作,效率更高。
这样,在绘制过后,其实还有一个合并的过程,将 CPU 和 GPU 处理的图片合到一起。
所以,一个完整的浏览器处理结构图就出来了:
参考: