这是我参与更文挑战的第3天,活动详情查看: 更文挑战
阅读浏览器工作原理(二)
将请求的内容呈现在浏览器上,默认显示html、xml内容,还可以通过插件拓展显示pdf等内容。
总流程
- 从网络层获取请求文档内容
- 解析文档
- html文档 ->(gecko会形成内容槽content sink,用于生成dom元素)-> 形成dom树
- 解析css->形成css规则树
- 两者结合(attachment)形成呈现树(gecko称为框架树)
- 布局,为每个节点分配呈现坐标(gecko称为重排)
- 绘制,渲染引擎遍历呈现树,用户界面后端绘制每个节点
解析
解析分为两个部分:词法分析器负责词法分析、解析器负责语法分析。词法分析就是把文本分割成标记,语法分析就是应用语法规则去分析文档结构,从而构建解析树。
词汇通常用正则表达式定义,而语法通常用bnf格式定义。
解析器首先向词法分析器请求标记,然后将它和语法规则进行匹配。如果匹配到了某条规则就会将该标记(如html)对应的节点(生成html元素节点并添加)添加到解析树中,然后请求下一个标记。如果找不到匹配规则,就报语法错误。
解析器分为自上而下的解析器(起点总是最高级别的规则,比如说一个字符是项,项+项=表达式,那么就会直接匹配表达式)和自下而上的解析器(一项一项的扫描并匹配规则)。
HTML解析器
html无法用常规解析器来解析,也不能用xml解析器来解析,因为他比较宽容,能够包容很多语法错误。HTML4基于SGML,所以采用dtd规定标记,其中严格模式(标准模式)下完全遵守html规范,但是其他模式可以兼容一些以前的浏览器标记(怪异模式),HTML5不基于SGML,所以只有一种dtd声明:
DOM
HTML解析器输出的解析树是DOM树。
html5规范中,解析流程为:标记化(词法分析)-> 树构建 -> 形成dom树。其中如果在树构建阶段执行了document.write,就会回到标记化阶段去添加额外的标记。
标记化算法
<html>
<body>
hello
</body>
</html>
一开始是数据状态
-
遇到<:转为标记打开状态
遇到a-z字符:创建一个起始标记,同时转为标记名称状态
遇到>:发送标记html,转为数据状态
-
<body>标签同上
-
遇到h,创建字符标记并发送。
遇到e,创建字符标记并发送....
-
遇到<,转为标记打开状态
遇到/,创建结束标记,转为标记名称状态
遇到>,发送标记body,转为数据状态
-
</html>标签同上
树构建算法
以document为根节点,不断根据标记创建并添加各种元素。这些元素还会被添加到一个堆栈中,可以纠正嵌套错误和处理未关闭的标记。
<html>
<body>
hello
</body>
</html>
-
initial mode状态
-
接受html标记 -> before html状态
创建HTMLhtmlElement元素,添加document对象上
-> before head状态
-
接受body标记
隐式创建一个HTMLHeadElement,添加到树上
-> after head状态
创建HTMLBodyElement,添加到树上
-> in body状态
-
接受h
创建一个text节点,添加到树上,其他字符也都添加到该节点上
-
接受body结束标记 -> after body状态
-
接受html结束标记 -> after after body状态
解析结束后,会将文档状态设置为完成。
以chrome为例构建DOM树
chrome构建dom的基本流程:
名词解释:
HTMLDocumentParser:负责将html文本解析成一些标签文本(tokens)
HTMLTreeBuilder:对tokens进行分类处理,然后根据不同类型调用HTMLConstructionSite的不同函数来构建dom
HTMLConstructionSite:对不同类型标签创建不同html,并且建立父子关系。其中的m_docunebt变量就是window.document。
流程解释:
-
发送url请求之后,会通过ipc进程间通信,触发DocumentLoader。
其中会初始化HTMLDocumentParser,并且实例化documment对象。
之后将收到的数据(html文本)带给HTMLDocumentParser去解析,形成tokens。
-
形成tokens
chrome定义7种标签类型:
enum TokenType { Uninitialized, DOCTYPE, // <!DOCTYPE html> StartTag, // 起始标签<div>等 EndTag, // 结束标签</div>等 Comment, //注释 Character, //字符串 EndOfFile, //文档结束 };以一个html文本为例:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <div> <h1 class="title">demo</h1> <input value="hello"> </div> </body> </html>每一段文本对应的token如下:
<!DOCTYPE html> <!--tagname: html,type: DOCTYPE--> <html> <!--tagname: html,type: startTag--> <!--text: \n,type: Character --> <head> <!--tagname: head,type: startTag--> <!--text: \n,type: Character --> <meta charset="utf-8"> <!--tagname: meta,type: startTag--> <!--text: \n,type: Character --> </head> <!--tagname: head,type: EndTag--> <!--text: \n,type: Character --> <body> <!--tagname: body ,type: EndTag--> <!--text: \n,type: Character --> <div> <!--tagname: div ,type: startTag--> <!--text: \n,type: Character --> <h1 class="title"> <!--tagname: h1 ,type: startTag--> demo <!--test: demo ,type: Character--> </h1> <!--tagname: h1 ,type: EndTag--> <!--text: \n,type: Character --> <input value="hello"> <!--tagname: input ,type: startTag,attr:value=hello--> <!--text: \n,type: Character --> </div> <!--tagname: div ,type: endTag--> <!--text: \n,type: Character --> </body> <!--tagname: body ,type: endTag--> <!--text: \n,type: Character --> </html> <!--tagname: html ,type: endTag--> <!--text: \n,type: Character --> <!--type: EndOfFile -->到这里已经形成了document,和一些格式化好的token,即将开始构建dom树。
-
构建dom树
主要是做什么:
建立父子、兄弟节点关系。以p标签HTMLParagraphElement为例:(图片来自参考文章)
- treeScope:记录属于哪个document
- Node:最顶层的父类,有三个指针:父节点、前一个节点、后一个节点
- ContainerNode:继承自node,添加两个指针:第一个子元素,最后一个子元素
- Element:添加关于attribute相关、clientWidth、scrollTop等函数
- HTMLElement:draggable,setDraggable等方法
- HTMLParagraphElement:创建元素
处理token:
void HTMLTreeBuilder::processToken(AtomicHTMLToken* token) { if (token->type() == HTMLToken::Character) { //文本节点 processCharacter(token); return; } switch (token->type()) { //doctype节点 case HTMLToken::DOCTYPE: processDoctypeToken(token); break; case HTMLToken::StartTag: //开标签 processStartTag(token); break; case HTMLToken::EndTag: //闭标签 processEndTag(token); break; //othercode } }-
processDoctypeToken
负责创建doctype节点,然后创建一个插dom的任务,最后设置文档类型。
如果tagName不是html或没有声明,那么就是怪异模式。如果是html4写法,由于有systemId:w3.org..,被判断为有限怪异模式。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">怪异模式:模拟ie(input,textarea的盒模型为border-box、标准模式下文档高度由内容撑开,怪异模式下文档高度为窗口可视区域。)而且css解析比较宽松。
有限怪异模式:和标准模式的唯一区别在于inline的行高处理不一样(div里面的图片下方不会留白,标准模式下会留白,空白处为div的行高),标准模式会遵循文档规定。
-
processStartTag
处理标签的时候:
-
先创建html节点(HtmlElement元素)
-
加入任务队列,传入当前节点和父节点(openElements.topNode)
主要是创建一个task,然后记录父节点和当前节点,如果超过dom树的最大深度(512)就会把当前节点当做父元素的同级节点。
-
压栈,该栈存放未遇到闭标签的开标签(用openElements建立父子关系)
-
执行队列里面的任务(用lastChild建立兄弟关系)
根据task的类型执行不同操作,先检查是否支持子元素,不支持就直接返回,不进行插入操作。
接下来设置子元素父节点、firstChild、lastChild。html的父节点即document。如果发现没有lastChild,就将子元素作为firstChild。但是由于document下已经有了docype节点,所以该元素的previousSibling就是上一个lastChild(docype),上一个lastChild(docype)的nexSibling就是这个html节点,最后再把该节点设置为lastChild。
处理的时候同上,创建head节点,插入队列,压栈时栈顶是,所以他就是的子节点。
-
-
processEndTag
遇到闭标签的时候会把栈里面的元素pop出来,直到看到第一个和标签名字一样的。
当我们把head标签pop出来,栈顶元素就又变成html,下一个插入的body元素就成了他的子节点。
但是关闭body标签的时候不会pop出来,如果body闭标签之后又写了标签就还是会被当成body的子元素。
<div> <p><b>hello</b></p> <p>demo</p> </div>如果不写,最后形成的结构是
<div> <p><b>hello</b></p> <!--后面会不断插入b标签--> <b><p>demo</p></b> </div>因为(a, b, big, code, em, font, i, nobr, s, small, strike, strong, tt, u)这样带有格式化的标签会特殊处理,遇到时会放到一个列表里面,如果遇到</b>就从列表里删掉。之后每次处理新标签(如
)就会检查和列表和栈里的是否对应,不对应的话就会重新插入一个,直到遇到下一个</b>。
-
关于自定义标签
会被实例化为HTMLUnknownElement,只提供create函数,也继承自HTMLElement,与span表现相同。
可以用js注册自定义标签:
class HighSchoolElement extends HTMLElement{ constructor(){ super(); this._country = null; } get country(){ return this._country; } set country(country){ this.setAttribute("country", _country); } static get observedAttributes() { return ["country"]; } attributeChangedCallback(name, oldValue, newValue) { this._country = newValue; this._updateRender(name, oldValue, newValue); } _updateRender(name, oldValue, newValue){ console.log(name + " change from " + oldValue + " " + newValue); } } window.customElements.define("high-school", HighSchoolElement); //<high-school country="China">NO. 2 high school</high-school>让该标签继承自htmlElement,可以直接取到country,而不用通过getAttribute去获取属性,还可以实现在属性变化时更新元素渲染。
这种方式创建之后就不是HTMLUnknownElement,v8会把构造函数转为c++函数,实例化一个HTMLElement对象。
CSS解析
css是上下文无关的语法,可以用常规解析器解析。webkit使用Flex(创建词法分析器,需要输入正则表达式定义的标记文件)和Bison(创建自下而上的解析器,需要输入BNF格式的语法规则)解析器生成器,解析器会将css文件解析成styleSheet对象,而且每个对象都包含cssRule,cssRule里面会包含选择器selector(如p、div)和声明对象(margin-top)等。
以chrome为例解析css
-
形成tokens
css的token很详细,具体定义点此查看,举例如下:(图片来自参考文章)
如果使用rgb,会被归类为函数类型的function-token
-
构建css规则树
对于以下css内容:
.text .hello{ color: rgb(200, 200, 200); width: calc(100% - 20px); } #world{ margin: 20px; }-
解析选择器
blink定义了几种matchType
enum MatchType { Unknown, Tag, // Example: div Id, // Example: #id Class, // example: .class PseudoClass, // Example: :nth-child(2) PseudoElement, // Example: ::first-child PagePseudoClass, // Example: :hover AttributeExact, // Example: E[foo="bar"] AttributeSet, // Example: E[foo] AttributeHyphen, // Example: E[foo|="bar"] AttributeList, // Example: E[foo~="bar"] AttributeContain, // css3: E[foo*="bar"] AttributeBegin, // css3: E[foo^="bar"] AttributeEnd, // css3: E[foo$="bar"] FirstAttributeSelectorMatch = AttributeExact, }几种关系型选择器类型
enum RelationType { SubSelector, // No combinator Descendant, // "Space" combinator Child, // > combinator DirectAdjacent, // + combinator IndirectAdjacent, // ~ combinator // Special cases for shadow DOM related selectors. ShadowPiercingDescendant, // >>> combinator ShadowDeep, // /deep/ combinator ShadowPseudo, // ::shadow pseudo element ShadowSlot // ::slotted() pseudo element };(题外话:关于Shadow DOM,可以看想了解Shadow DOM?看这里。)
所以上面的css解析结果为:
selector text = “.text .hello” value = “hello” matchType = “Class” relation = “Descendant” tag history selector text = “.text” value = “text” matchType = “Class” relation = “SubSelector” selector text = “#world” value = “world” matchType = “Id” relation = “SubSelector”其中selector text为识别到的选择器字符串,value为当前匹配到的选择器,matchType为选择器匹配类型,relation为关系型选择器类型。
可以看出解析是从右往左的,方便它判断父选择器。如果有一个元素的classname刚好是hello,那么他会判断该元素的所有父元素,是否有一个classname刚好是text的。
-
解析声明对象
在解析的时候,每一个属性都对应一个唯一id,包括:
enum CSSPropertyID { CSSPropertyColor = 15, CSSPropertyWidth = 316, CSSPropertyMarginLeft = 145, CSSPropertyMarginRight = 146, CSSPropertyMarginTop = 147, CSSPropertyMarkerEnd = 148, }所以最后解析出来的声明对象结果为:
selector text = “.text .hello” perperty id = 15 value = “rgb(200, 200, 200)” perperty id = 316 value = “calc(100% – 20px)” selector text = “#world” perperty id = 147 value = “20px” perperty id = 146 value = “20px” perperty id = 144 value = “20px” perperty id = 145 value = “20px” //可以看出这里会把margin分成4个属性 -
选择器+声明对象构成一个cssRule,Rule构成cssStyleSheet(图片来自参考文章)
用户定义的styleSheet会被存放到m_authorStyleSheets中。浏览器的默认样式会被存放到DefaultStyleSheet中,包括如把style/link/script等标签display: none,把div/h1/p等标签display: block,设置p/h1/h2等标签的margin值等。
如果是怪异模式,blink还会加载怪异模式表,里面设置了input、textarea的盒模型为border-box。
-
生成哈希map
CompactRuleMap m_idRules; CompactRuleMap m_classRules; CompactRuleMap m_tagRules; //标签 CompactRuleMap m_shadowPseudoElementRules; //伪类把生成的rule集放到四个类型的hashmap上,每个rule属于什么类型由最后一个selector决定。
这样可以方便快速取出可以匹配第一个(位置上是最右边那个)selector的所有rule,然后检查每条rule里面的每一级selector是否全都符合该元素的环境。
-
解析顺序
css是异步加载,不影响dom的构建,可能在css加载完成前DOM就已经构建好了。(但是在css加载完成前页面会一直白屏,不然会出现抖动造成不好的体验)。但是在没加载解析css的时候,为了防止脚本访问到错误的css数据,webkit会在脚本尝试访问的属性可能受到该影响时,禁止脚本的运行。