Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【笔记】How browsers work #5

Open
zhuzhuaicoding opened this issue Apr 9, 2017 · 0 comments
Open

【笔记】How browsers work #5

zhuzhuaicoding opened this issue Apr 9, 2017 · 0 comments

Comments

@zhuzhuaicoding
Copy link
Owner

zhuzhuaicoding commented Apr 9, 2017

浏览器架构

layers

渲染引擎

在浏览器屏幕上显示请求的内容
Webkit(Safari)
Blink(Chrome & Opera) a fork of WebKit.
Gecko(firefox)

主流程

flow

webkitflow

image008

解析器和词法分析器的组合

解析的过程可以分成两个子过程:词法分析和语法分析。
词法分析是将输入内容分割成大量标记的过程。标记是语言中的词汇,即构成内容的单位。在人类语言中,它相当于语言字典中的单词。

语法分析是应用语言的语法规则的过程。

解析器通常将解析工作分给以下两个组件来处理:词法分析器(有时也称为标记生成器),负责将输入内容分解成一个个有效标记;而解析器负责根据语言的语法规则分析文档的结构,从而构建解析树。词法分析器知道如何将无关的字符(比如空格和换行符)分离出来。
image011
它是一个迭代的过程。解析器会向词法分析器请求一个新标记,并尝试将其与某条语法规则进行匹配。如果发现了匹配规则,解析器会将一个对应于该标记的节点添加到解析树中,然后继续请求下一个标记。
如果没有规则可以匹配,解析器就会将标记存储到内部,并继续请求标记,直至找到可与所有内部存储的标记匹配的规则。如果找不到任何匹配规则,解析器就会引发一个异常。这意味着文档无效,包含语法错误。

翻译

很多时候,解析树还不是最终产品。解析通常是在翻译过程中使用的,而翻译是指将输入文档转换成另一种格式。编译就是这样一个例子。编译器可将源代码编译成机器代码,具体过程是首先将源代码解析成解析树,然后将解析树翻译成机器代码文档。
image013

HTML parser (HTML解析器)

解析HTML markup -> 解析树

HTML语法的定义

规范文档

上下文有关的语法

  • 解析器可以用BNF格式定义,很遗憾,所有的常规解析器都不适用于 HTML(我并不是开玩笑,它们可以用于解析 CSS 和 JavaScript)
  • 定义HTML的格式:DTD
  • HTML的处理是“宽容”的。它允许您省略某些隐式添加的标记,有时还能省略一些起始或者结束标记等等。

HTML DTD

  • SGML family
  • HTML DTD 无法构成与上下文无关的语法
  • 严格模式,其他模式。 规范

DOM

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

解析算法

  • HTML 无法用常规的自上而下或自下而上的解析器进行解析。

语言的宽容本质。
浏览器历来对一些常见的无效 HTML 用法采取包容态度。
解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,脚本标记如果包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容。

  • 自定义的解析器来解析HTML。
  • HTML5 规范详细地描述了解析算法。此算法由两个阶段组成:标记化和树构建。
    标记化:词法分析过程,将输入内容解析成多个标记。HTLM标记包括起始标记,结束标记,属性名称,属性值。
    标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;如此反复直到输入的结束。
    image017

标记化算法

  1. 输出为HTML标记
  2. 状态机。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即使接收的字符相同,对于下一个正确的状态也会产生不同的结果,具体取决于当前的状态。
    Tag open state -> tag name state -> data state

树构建算法

  1. 创建解析器的同时,也会创建Document object。以document为root,DOM树会不断修改,新的元素会添加到DOM树上。标记生成器发送的每个节点都会由树构建器进行处理(Each node emitted by the tokenizer will be processed by the tree constructor.)。规范中定义了每个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时创建。这些元素不仅会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也可以用状态机来描述。这些状态称为“插入模式”。
    树构建阶段的输入是一个来自标记化阶段的标记序列。initial mode -> before html -> before head -> in head -> after head -> in body ->after body -> after after body -> EOF token。

解析后的动作

  1. 文档状态为交互状态(interactive ),开始解析deferred的脚本(也就是也就是那些应在文档解析完成后才执行的脚本)。然后,设置为“complete”,触发“load”事件

浏览器容错机制

  1. 新添加的元素不允许加到外部标记里。在此情况下,我们应该关闭所有标记,直到出现禁止添加的元素,然后再加入该元素。
  2. 我们不能直接添加的元素。这很可能是网页作者忘记添加了其中的一些标记(或者其中的标记是可选的)。这些标签可能包括:HTML HEAD BODY TBODY TR TD LI(还有遗漏的吗?)。
  3. 向 inline 元素内添加 block 元素。关闭所有 inline 元素,直到出现下一个较高级的 block 元素。
    如果这样仍然无效,可关闭所有元素,直到可以添加元素为止,或者忽略该标记。
  • </br> instead of <br>
  • 离散表格
<table>
    <table>
        <tr><td>inner table</td></tr>
    </table>
    <tr><td>outer table</td></tr>
</table>

=>

<table>
    <tr><td>outer table</td></tr>
</table>
<table>
    <tr><td>inner table</td></tr>
</table>

Implemention

if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);
  • 嵌套的表单
if (!m_currentFormElement) {
        m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}
  • 深层次的结构
    对同类型标签嵌套深度有限制,超过了会忽略
    代码实现:
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{

unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
         i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
     curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}
  • 放错位置的 html 或者 body 结束标记
if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

CSS解析

构建CSSOM树,增量构建,一次渲染
qq 20170410130859

  1. 上下文有关语法
  2. 词法(文法)是针对各个标记用正则表达式定义的。
  3. 语法(文法)是BNF格式描述的。
ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;

规则集:

div.error , a.error {
  color:red;
  font-weight:bold;
}

WebKit CSS 解析器

  • WebKit 使用 Flex 和 Bison 解析器生成器,通过 CSS 语法文件自动创建解析器。
    Bison 会创建自下而上的移位归约解析器。Firefox 使用的是人工编写的自上而下的解析器。这两种解析器都会将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。
    image017

处理脚本和样式表的顺序

  1. 脚本
    网络的模型是同步的。网页作者希望解析器遇到 <script> 标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。作者也可以将脚本标注为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行。
  2. 预解析
    WebKit 和 Firefox 都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
  3. 样式表
    另一方面,样式表有着不同的模型。理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
  4. render树构建
    • 构建完dom树,继续构建render树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。
    • Firefox 将呈现树中的元素称为“框架”。WebKit 使用的术语是render或render object。
    • render知道如何布局并将自身及其子元素绘制出来。webkit有 RenderObject类,render的基类。
class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer
}
  • 每一个呈现器都代表了一个矩形的区域,通常对应于相关节点的 CSS 框,这一点在 CSS2 规范中有所描述。它包含诸如宽度、高度和位置等几何信息。
  • 框的类型会受到与节点相关的“display”样式属性的影响(请参阅样式计算章节)。下面这段 WebKit 代码描述了根据 display 属性的不同,针对同一个 DOM 节点应创建什么类型的呈现器。
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
    Document* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    RenderObject* o = 0;

    switch (style->display()) {
        case NONE:
            break;
        case INLINE:
            o = new (arena) RenderInline(node);
            break;
        case BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case INLINE_BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case LIST_ITEM:
            o = new (arena) RenderListItem(node);
            break;
       ...
    }

    return o;
}
  • 元素类型也是考虑因素之一,例如表单控件和表格都对应特殊的框架。通过重写createRenderer方法。
  1. render树和 DOM 树的关系
    • 对应,并非一一对应。非可视化的 DOM 元素不会插入呈现树中,例如“head”元素。如果元素的 display 属性值为“none”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden”的元素仍会显示)。

    • 有些DOM元素对应多个RenderObject

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

    • 一些渲染对象和所对应的Dom节点不在树上相同的位置,例如,浮动和绝对定位的元素在文本流之外,在两棵树上的位置不同,渲染树上标识出真实的结构,并用一个占位结构标识出它们原来的位置。
      image025

    • 初始块容器在firefox里叫做“viewport”;在webkit里,叫“RenderView”对象。

构建render树的流程

  • Firefox使用FrameConstructor,解析样式,创建frame
  • Webkit使用attach,解析样式,创建render
  • 处理 html 和 body 标记就会构建呈现树根节点。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。Firefox 称之为 ViewPortFrame,而 WebKit 称之为 RenderView。这就是文档所指向的呈现对象。呈现树的其余部分以 DOM 树节点插入的形式来构建。
  • 请参阅关于处理模型的 CSS2 规范。

样式计算

  1. 构建呈现树时,需要计算每一个呈现对象的可视化属性。这是通过计算每个元素的样式属性来完成的。
  2. 样式包括来自各种来源的样式表、inline 样式元素和 HTML 中的可视化属性(例如“bgcolor”属性)。其中后者将经过转化以匹配 CSS 样式属性。样式表的来源包括浏览器的默认样式表、由网页作者提供的样式表以及由浏览器用户提供的用户样式表
  3. 样式计算的难点:
    • 样式数据是一个超大的结构,存储了无数的样式属性,这可能造成内存问题。
    • 如果不进行优化,为每一个元素查找匹配的规则会造成性能问题。要为每一个元素遍历整个规则列表来寻找匹配规则,这是一项浩大的工程。选择器会具有很复杂的结构,这就会导致某个匹配过程一开始看起来很可能是正确的,但最终发现其实是徒劳的,必须尝试其他匹配路径。

例如下面这个组合选择器:div div div div{ ... }
这意味着规则适用于作为 3 个 div 元素的子代的

。如果您要检查规则是否适用于某个指定的
元素,应选择树上的一条向上路径进行检查。您可能需要向上遍历节点树,结果发现只有两个 div,而且规则并不适用。然后,您必须尝试树中的其他路径。
- 应用规则涉及到相当复杂的层叠规则(用于定义这些规则的层次)。

让我们来看看浏览器是如何处理这些问题的:

  • 样式数据共享
    Webkit引用RenderStyle对象。
  • Firefox规则树
    为了简化样式计算,Firefox 还采用了另外两种树:规则树和样式上下文树。WebKit 也有样式对象,但它们不是保存在类似样式上下文树这样的树结构中,只是由 DOM 节点指向此类对象的相关样式。
    样式上下文包含计算后的值。要计算出这些值,应按照正确顺序应用所有的匹配规则,并将其从逻辑值转化为具体的值。例如,如果逻辑值是屏幕大小的百分比,则需要换算成绝对的单位。规则树的点子真的很巧妙,它使得节点之间可以共享这些值,以避免重复计算,还可以节约空间。

所有匹配的规则都存储在树中。路径中的底层节点拥有较高的优先级。规则树包含了所有已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为所有的节点进行计算,而是只有当某个节点样式需要进行计算时,才会向规则树添加计算的路径。

这个想法相当于将规则树路径视为词典中的单词。如果我们已经计算出如下的规则树:
tree
假设我们需要为内容树中的另一个元素匹配规则,并且找到匹配路径是 B - E - I(按照此顺序)。由于我们在树中已经计算出了路径 A - B - E - I - L,因此就已经有了此路径,这就减少了现在所需的工作量。
让我们看看规则树如何帮助我们减少工作。

结构划分

分割成多个结构。这些结构包含了特定类别(如 border 或 color)的样式信息。结构中的属性都是继承的或非继承的。继承属性如果未由元素定义,则继承自其父代。非继承属性(也称为“重置”属性)如果未进行定义,则使用默认值。规则树通过缓存整个结构(包含计算出的端值)为我们提供帮助

使用规则树计算样式上下文

  • 在计算某个特定元素的样式上下文时,我们首先计算规则树中的对应路径,或者使用现有的路径。然后我们沿此路径应用规则,

  • 在新的样式上下文中填充结构。我们从路径中拥有最高优先级的底层节点(通常也是最特殊的选择器)开始,并向上遍历规则树,直到结构填充完毕。如果在那个规则节点上没有定义结构规则,则沿着路径向上,直到找到该结构规则。整个结构都能共享。这可以减少端值的计算量并节约内存。

  • 如果我们找到了部分定义,就会向上遍历规则树,直到结构填充完毕。

  • 如果我们找不到结构的任何定义,那么假如该结构是“继承”类型,我们会在上下文树中指向父代的结构,这样也可以共享结构。如果是 reset 类型的结构,则会使用默认值。

  • 如果最特殊的节点确实添加了值,那么我们需要另外进行一些计算,以便将这些值转化成实际值。然后我们将结果缓存在树节点中,供子代使用。

  • 如果某个元素与其同级元素都指向同一个树节点,那么它们就可以共享整个样式上下文。

  • webkit没有规则树。遍历4次,根据正确的层叠顺序进行解析。最终出现的最终生效。

样式层叠顺序

布局

  • 计算需要渲染的节点的大小和位置
  • 节点位置和大小是基于viewport计算的。
  • 在移动端通常将viewport设为浏览器推荐的理想视口,以保证字体显示大小易于阅读
  • 旋转屏幕、修改浏览器窗口大小,修改位置大小相关的CSS属性,都可能触发Layout
  • 有个layout或者reflow方法
  • HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历
  • Dirty位系统
    • "dirty",
    • "children are dirty"
  • 增量方式。只对 dirty 呈现器进行布局。当render为 dirty 时,会异步触发增量布局。

异步布局和同步布局

增量布局是异步执行的。
请求样式信息(例如“offsetHeight”)的脚本可同步触发增量布局。
全局布局往往是同步触发的。
有时,当初始布局完成之后,如果一些属性(如滚动位置)发生变化,布局就会作为回调而触发。

优化

  • 大小调整,render object的位置改变而触发的,从缓存中获取render的大小
  • 只有一个子树进行了修改,因此无需从根节点开始布局。比如在文本字段中插入文本 。

布局过程

  1. 父呈现器确定自己的宽度。
  2. 父呈现器依次处理子呈现器,并且:
    1.放置子呈现器(设置 x,y 坐标)。
  3. 如果有必要,调用子呈现器的布局(如果子呈现器是 dirty 的,或者这是全局布局,或出于其他某些原因),这会计算子呈现器的高度。
  4. 父呈现器根据子呈现器的累加高度以及边距和补白的高度来设置自身高度,此值也可供父呈现器的父呈现器使用。
  5. 将其 dirty 位设置为 false。

计算宽度

  • render的宽度根据容器块的宽度、呈现器样式中的“width”属性以及边距和边框计算得出的。
  • 得到的是“preferred width”。还需要计算最小宽度和最大宽度。
  • 值会缓存,以用于需要布局而宽度不变的情况。

换行

如果呈现器在布局过程中需要换行,会立即停止布局,并告知其父代需要换行。父代会创建额外的呈现器,并对其调用布局。

绘制

  • 根据background, border, box-shadow等样式,将Layout生成的区域填充为最终将显示在屏幕上的像素
  • 全局绘制和增量绘制
  • 绘制顺序
    1. CSS2 规范定义了绘制流程的顺序。。绘制的顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块呈现器的堆栈顺序如下: 背景颜色->背景图片->边框->子代->轮廓

render线程 单线程

网络线程一般2~6个

事件循环

浏览器的主线程是事件循环。它是一个无限循环,永远处于接受处理状态,并等待事件(如布局和绘制事件)发生,并进行处理。

CSS2可视化模型

  1. 画布
  2. box model
    block,inline
  3. 定位
  • 普通
  • 浮动
  • 绝对
    box布局方式是由以下因素决定的:
  • box类型
  • box尺寸
  • 定位方案
  • 外部信息,例如图片大小和屏幕大小
  1. 盒类型
    块盒,内联盒
    block 采用的是一个接一个的垂直格式,而 inline 采用的是水平格式。
    inline 框放置在行内或“行框”中。行至少会和其中最高的inline box一样高,当box以baseline对齐时——即一个元素的底部和另一个box上除底部以外的某点对齐,行高可以比最高的inline box高。

定位

相对
浮动
绝对定位,fixed定位

Refs:

  1. https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
  2. http://ppt.baomitu.com/d/258e0812#/1
    https://t.75team.com/video/play?id=65_260_2017032010370819aadb47-c79c-4d63-afd4-df0b42eaee48
  3. http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/
@zhuzhuaicoding zhuzhuaicoding changed the title How browsers work 笔记:How browsers work Apr 9, 2017
@zhuzhuaicoding zhuzhuaicoding changed the title 笔记:How browsers work How browsers work Apr 9, 2017
@zhuzhuaicoding zhuzhuaicoding changed the title How browsers work 【笔记】How browsers work Apr 9, 2017
@zhuzhuaicoding zhuzhuaicoding mentioned this issue Nov 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant