手记

渲染引擎与 HTML 编译器

渲染引擎

不管是通过网络传输而来的,还是缓存在本地磁盘的,资源最初都是以字节(1)流的形式存在的。将字节流转变为可视化的页面,是浏览器最核心的功能。
在浏览器中,能够将 HTML、CSS、JavaScript 以及其他资源( 图片、视频等 )转换成可视化页面的模块,叫渲染引擎。

(1)字节( Byte )是二进制数据的单位,按照 UTF-8 编码标准,一个英文字母等于一个字节,一个汉字等于三个字节。

渲染引擎的主要功能

  1. 调用 HTML 编译器
  2. 调用 CSS 编译器
  3. 调用 JavaScript 编译器
  4. 调用其他资源( 图片、视频等 )解码器
  5. 布局
  6. 绘图

多线程

渲染引擎的主线程是 GUI 渲染线程,负责布局和绘图,而其他资源的数据都交给不同线程去完成,比如,网页交给 HTML 编译器转变成一系列的词语,JavaScript 代码调用 JavaScript 引擎编译和执行。

主流渲染引擎

目前,主流的渲染引擎包括 Trident( IE 浏览器 )、Gecko( 火狐浏览器 )和 WebKit( Chrome 浏览器、Safari 浏览器、Android 浏览器 )。

HTML 编译器

HTML 编译器的工作就是将网络或者本地磁盘获取的 HTML 网页和资源从字节流编译成 DOM 树结构。这一过程大致可以理解成下图所述的步骤:

解码:

HTML 编译器首先要做的事情是如何正确地解码字节流。服务器一般会通过响应头里的 Content-Type 字段,告知编译器字符的编码格式。

例子:

//text/html 表示文件格式
//charset=UTF-8 表示字符编码格式
Content-Type: text/html; charset=UTF-8

如果服务器没有返回 Content-Type 的信息,编译器会自动匹配 HTML 文档中设置的编码格式( 写在 <meta> 标签中 )。通常,编译器会等待500ms或1024字节,以提高匹配到编码格式的可能性。因此,HTML 文档中的字符编码声明要出现在前1024个字节中。

例子:

<meta charset="UTF-8">

如果以上算法都不能确定解码方式,编译器会调用默认字符编码。默认编码通常取决于用户的所在区域,或用户网页常用的语言,例如 GB18030
是默认的汉字编码标准。

词法分析

这个过程会将字符流分解成有意义的词法单元( token )。

例子:

<p class="style_1">hello</p >

如果我们从" 最小且有意义 "的单元来拆分这段代码( 字符流 ),可以把它依次拆分成如下这样:

<p:"开始标签"的开始;
class="style_1":属性
> :"开始标签"的结束
hello:文本
</p>: 结束标签

那么,渲染引擎是如何实现拆分的呢?大多数编程语言的词法分析都是通过" 状态机 "来实现的。

所谓状态机,简单说,就是把每一个流入的字符都标记上状态,并根据当前字符状态,标记下一个字符的状态,然后把相关字符合并起来,返回有意义的词语。

例子中,初始状态为" 数据状态 “;编译器获得字符 < ,状态改为” 标签打开状态 “;之后获得字符 p ,状态改为” 开始标签状态 “;接下来编译器一直读,当读到空格时,状态恢复到” 数据状态 ",并返回完整的词 <p 。当然,实际的解析过程非常繁琐,数据状态也有八十种之多,我们在此只是简单了解一下。

语法分析

这个过程是将词法单元流转换成 DOM 节点,与此同时,构建一个由节点逐级嵌套的 DOM 树。经过语法分析,HTML 文档变成了一组允许 JavaScript 访问和交互的对象。

语法分析主要通过栈( 后进先出 )和插入模式来实现。这些节点会在接收到相应的" 开始标签状态 “的词时创建并入栈,然后在接收到” 结束标签状态 "的词时出栈。下面举例说明:

例子:

<div id="box">
    <p class="style_1">hello lover</p >
</div >

1、首先,Document 作为根节点,第一个被添加到 DOM 树上;

//DOM 树
{nodeName:"#document"}

2、第一个模式为" 初始模式 “;编译器获得” 开始标签 " 词<div ,插入模式切换为" 在DIV中 ",并在这个模式下将属性 id 及其对应的值 box 添加到该元素中。至此,DIV 节点被创建并添加到 DOM 树上。

//DOM 树
{nodeName:"#document"}
    {nodeName:"DIV",id:"box"}

3、接下来,编译器获得" 开始标签 " 词<p ,插入模式切换为" 在P中 ",并在这个模式下将属性 class 及其对应的值 style_1 添加到该元素中。至此,P 节点被创建并添加到 DOM 树上。

//DOM 树
{nodeName:"#document"}
    {nodeName:"DIV",id:"box"}
        {nodeName:"P",class:"style_1"}

4、然后,编译器获得" 文本标签 " 词hello ,插入模式切换为" 文本 ",并在这个模式下将文本 lover 合并,至此,文本节点被创建并添加到 DOM 树上。

//DOM 树
{nodeName:"#document"}
    {nodeName:"DIV",id:"box"}
        {nodeName:"P",class:"style_1"}
            {nodeName:"#text",nodeValue:"hello lover"}

5、到此为止,DOM 树已经构建完毕,编译器依次获得" 结束标签 " 词</p ></div >,插入模式依次切换为" 在P后 “和” 在DIV后 ",解析过程结束。

注意

上面的内容中,我们将" 语法分析 "粗略的归纳到 HTML 编译器的工作范畴,主要是为了方便记忆,其实按照 WebKit 渲染引擎的思想,词法分析这个阶段调用 HTML 编译器线程单独完成,而语法分析与构建 DOM 树则是在 GUI 渲染线程上完成的。

GUI 渲染线程与 JavaScript 引擎

在语法分析的时候( 创建节点和构建 DOM 树的同时 ),读到 JavaScript 代码或者获取到" Script "节点( 没有标记异步方式 ),GUI 渲染线程会被阻碍,DOM 树的创建没有办法继续往下进行,直到 JavaScript 的资源加载完毕并被 JavaScript 引擎执行后,才可以继续 DOM 树的创建。也可以说,GUI 渲染线程与 JavaScript 引擎只能交替执行。如果节点需要依赖其他资源,例如图片、CSS 等,默认是异步的,不会阻碍 DOM 树的创建。

改进方式

1、 在" script “节点加上” async "属性,取值为 true,表明这是一个可以异步执行的JavaScript 代码( IE 浏览器设置属性 defer )。

2、 将" script “节点放在” body "元素的最后。

3、 WebKit 渲染引擎在主线程被阻碍的时候,会启动一个线程去遍历后面的 HTML 内容,收集页面需要使用的其他资源,然后并发下载这些资源( 包括 JavaScript 资源 )。这种机制对于网页的加载速度提升很大。


如有错误,欢迎指正,本人不胜感激。

0人推荐
随时随地看视频
慕课网APP