浏览器幕后之渲染引擎

2,026 阅读5分钟

前言

一探浏览器幕后的《三俩事》上一篇的介绍让大家对浏览器的组成有了个模糊的认识:

  1. 浏览器是什么?
  2. 浏览器(chromium)基本架构
  3. 浏览器主要组件
  4. 浏览器内核是什么?
  5. JavaScript(js)引擎
  6. 渲染引擎 今天呢做渲染模块(WebKit)展开学习讨论。

首先作为顺序介绍来说。我应该具体介绍实现一个浏览器应该包含哪些内容(介绍一下chromium实现包含了哪些内容)。但是考虑在前端话题的介绍。只做简单的列举不做具体展开讨论;有兴趣可以自行下去学习。

其次按大家耳熟能详的程度来说的话我应该着重讨论一下JS引擎(V8 event-loop相关);但是本节内容还是想先介绍讨论一下渲染引擎(WebKit)。不为别的我就是喜欢玩~ 因为我写这个系列的受众还是偏向前端,所以涉及到需要代码介入讲解的部分这边采取JavaScript来实现。

渲染引擎

1621765871(1).jpg

画架构图画习惯了,不要在意丑陋的细节 - -. 由上图可以得到渲染引擎内部解析执行大概过程的信息:

  1. 当访问页面的时候会进行网络请求(network)去获取页面的内容;当如果命中缓存(cache)就会从存储(memory)上直接读取。
  2. HTMLParser 进行html解析通过特定标识进行分块。可解析为CSS DOM JS几个模块。
  3. CSSParser 进行css也是解析。
  4. DOMParser 进行DOM结构解析。(构建过程中发现是JS部分执行5,如果是资源进行异步请求.不会阻碍解析)。
  5. JavaScript 部分丢给JS引擎进行解析。(如果有操作DOM/CSS部分会影响之前的解析,同时会阻碍解析)。
  6. 通过上文解析内容构建RenderTree。
  7. 解析完成通过renderTree进行布局和绘制。
  8. 将最终图像显示在屏幕上。

下面对于上文所提部分进行展开讨论一下


Parser 解析

上面Parser的字眼是不是太多了。我们得到一个信息解析是渲染引擎中非常重要的一个环节,所以首先需要介绍一下解析.

解析文档是指将文档转化成为有意义的结构,也就是可让代码理解和使用的结构。解析得到的结果通常是代表了文档结构的节点树,它称作解析树或者语法树。

解析是以文档所遵循的语法规则(编写文档所用的语言或格式)为基础的。比方说HTML解析那么是在HTML4/5的规范上进行的。所有可以解析的格式都必须对应确定的语法(由词汇和语法规则构成)。

image011.png

解析过程通常是分成两个子过程:词法分析和语法分析词法分析是将输入内容分割成大量标记的过程。(进行切分可识别大量标记的词段)语法分析是应用语言的语法规则的过程(到底这段语言是要做什么工作)。

HTMLParser

HTML 解析器的任务是将 HTML 标记解析成解析树。

HTML 语法定义: HTML 的词汇和语法在 W3C 组织创建的规范中进行了定义。

DOM 解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构。DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。 解析树的根节点是Document对象。

image.png

DOM解析code示意(不能运行的) 首先要了解解析过程一定是迭代过程:

// 分析标记<>和属性的正则表达式
    var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[\w-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
        endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/,
        attr = /([-A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
    function ParserHTML(html){
         // 接收参数html
        while(html){

            // 匹配注释内容
            if(html.indexOf("<!--") == 0){
                //
            }
            // 匹配开始标签
            if (html.indexOf("<") == 0) {
                match = html.match(startTag);

                if (match) {
                  html = html.substring(match[0].length); //匹配截取
                  //继续迭代
                }
           }
           if(html.indexOf("</") == 0){
               //匹配结束标签操作
           }

        }
    }
    // 输出示意结构
    {
        element:"",
        parentNode:{},
        childrenNode:{},
        content:"",
        ....
    }
    // 我之前写的找不到了--, 大家可以试着去实现一下。有问题随时沟通 本示例正则可用。
    

CSSParser

CSS解析同HTML不同地方是上下文无关的语法;可通过很多解析器做解析。 词法语法(词汇)是针对各个标记用正则表达式定义的:

  • comment /*[^]*+([^/][^]*+)/
  • num [0-9]+|[0-9]*"."[0-9]+
  • nonascii [\200-\377]
  • nmstart [_a-z]|{nonascii}|{escape}
  • nmchar [_a-z0-9-]|{nonascii}|{escape}
  • name {nmchar}+
  • ident {nmstart}{nmchar}*

通过 CSS 语法文件自动创建解析器, 会创建自下而上的移位归约解析器。会将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。如下:

image023.png

RenderTree

在 DOM 树构建的同时,浏览器还会构建另一个树结构:渲染树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。

WebKits RenderObject 类是所有渲染树的基类,其定义如下:

class RenderObject{
  virtual void layout();  //布局
  virtual void paint(PaintInfo);  //绘制
  virtual void rect repaintRect(); //重新绘制Rect
  Node* node;  // DOM节点
  RenderStyle* style;  // 计算render style
  RenderLayer* containgLayer; //render layer 
}
渲染树和 DOM 树的关系

渲染树是和 DOM 元素相对应的,但并非一一对应。非可视化的 DOM 元素不会插入渲染树中,例如“head”元素。如果元素的 display 属性值为“none”,那么也不会显示在渲染树中(但是 visibility 属性值为“hidden”的元素仍会显示)。

关于多渲染树的例子是格式无效的 HTML。根据 CSS 规范,inline 元素只能包含 block 元素或 inline 元素中的一种。如果出现了混合内容,则应创建匿名的 block 渲染树,以包裹 inline 元素。

有一些渲染对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。浮动定位和绝对定位的元素就是这样,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是占位框架。

布局

创建完成渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。

HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历 (3.5)。

坐标系是相对于根框架而建立的,使用的是上坐标和左坐标。

布局是一个递归的过程。它从根渲染树(对应于 HTML 文档的 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的渲染树计算几何信息。

根渲染树的位置左上角 是 0,0,(跟canvas2D坐标规则一致)其尺寸为视口(也就是浏览器窗口的可见区域)。 所有的渲染树都有一个layout或者reflow方法,每一个渲染树将会调用其需要进行布局的子代的 layout 方法。

绘制

在绘制阶段,系统会遍历渲染树,并调用渲染树的paint方法,将渲染树的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。

绘制的顺序其实就是元素进入堆栈样式上下文的顺序;从后向前。块渲染树的堆栈顺序如下:

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. 子代
  5. 轮廓

绘制实现可参考canvas(canvas一定要学的啊 时刻关注一些前沿的动作),了解基础图元绘制方法与过程。例如绘制line(需要坐标 基础图元结构等等):

image.png

最后

距离上一篇文章的生活日记: 20210521那天呢照样开心(朋友圈依旧不活跃)。20210522呢不开心。20210523也就是今天总的来说过得去 并没有学习。(劳逸结合 哈哈)

然后呢因为最近有人总微信我编程范式到底选择哪种好一点呢? 我现在用的更多是什么编程范式呢? 自己写代码维护起来好困难怎么办呢?... (做了很久的解答,应该记录下来提供给大家)下期JS引擎之前 我会把编程范式做一个简单介绍供大家参考 小小插曲(预计明天)。

加油!有问题请大家随时留言,看到一定会回复的

相关参考

  1. CSS 的词法和语法
  2. 处理模型
  3. WebCore Rendering