当咱们去探索浏览器运行原理的时候,咱们倾向于执行多个例子去推断其内在的设计;在很长一段时间内,我也是这么去探索浏览器这个黑盒的,但这么作始终只是验证例子自己而不能断言浏览器实际的行为。为了追求真理(误),决定写这个系列的文章,从源头探索浏览器的行为准则。javascript
HTML Standard即由w3c制定的html规范,而咱们实际使用的浏览器的核心,诸如chrome内部的webkit,则是html规范的实现。全部界面行为(不包括浏览器自身元件如书签)都由HTML规范所描述,并由webkit(或者其余浏览器引擎)实现。须要注意的是Javascript规范并非由w3c制定的,HTML规范只定义了其中的Document和window对象,也就是常说的DOM和BOM,咱们接下来探究的就是HTML的规范是如何定义的。css
须要注意一些点:html
因为上述缘由,在非webkit内核浏览器测试本文用例可能会有问题。前端
准确的说浏览器不仅能解析HTML,还能解析包括XML文档、大部分图片格式、PDF文件等等,可是咱们如今只关注HTML是如何被解析的。java
在开始探讨浏览器解析html的流程以前,先对Dom进行一下定义:node
本文将HTML文本生成Dom的过程称为解析(Parser),将Dom Tree + Css生成Layout Tree再进行绘制的过程称为渲染(Render)。react
Dom对于浏览器而言,就像Virtual Dom于前端开发者,Dom并非真实的视图(咱们所看到的界面),但浏览器能够根据Dom Tree和Css计算出底层绘制指令。git
而HTML Parser产出的是Dom,而不是实际的界面,且Dom的变化并不会马上致使绘制(相似react调用setState)。github
因此当HTML Parser阻塞意味着暂停产出新的节点(eg: HTMLElement),有些时候HTML Parser阻塞并不表明浏览器没法绘制界面。(若是已经存在部分Dom和Css我为何不能绘制?)web
让咱们一步步揭开浏览器的神秘面纱。
当咱们打开一个网页的时候,实际上浏览器发起了一个请求,最终将请求结果呈现给用户。做为开发者,咱们关心的是:在这个过程当中浏览器应该进行怎么样的准备工做?是如何去处理请求的响应的?带着这两个疑问,咱们继续往下看。
那么浏览器是如何作的呢?浏览器在请求资源之初会初始化一个独立的上下文称为browsing context包含了:
当请求的资源是一个HTML文件的时候(浏览器使用content-type识别),浏览器会初始化一个HTML Parser关联到当前的document,并将响应结果传入给HTML Parser进行解析,这是HTML Standard中规定的parser流程:
咱们从file system或者http response中拿到是字节流,拿到字节流后浏览器会尝试去decode,会根据以下的设定选取解码器:
此外规范还推荐使用兼容ascii编码的编码(例如utf-8)(vsc默认使用该编码保存文档)去编写HTML文档,由于规范使用ascii编码探测meta标签,进而获取文档的编码。
一个使用了错误编码解析的文档:
至此,咱们的浏览器终于能正确的将字节流decode了,可是在处理字符构建Dom以前,还须要额外的预处理,称为Input Stream Preprocessor,这个步骤执行的作的事情仅仅是标准化换行符,由于在不一样系统下使用文本使用的换行符是不一致的,例如Windows使用CRLF做为换行符,而类Unix系统使用LF做为换行符。
除此以外,咱们看到Script Execution步骤经过调用document.write回流到Input Stream Preprocessor(这也是为何脚本执行会阻塞浏览器解析的缘由),顾名思义在脚本执行阶段,document.write插入的内容会在注入到Input Stream中,而且做为下一个解析点。咱们来看看效果:
<!DOCTYPE html>
<html lang="en">
...
<body>
<p>
parser first
</p>
<script> document.write('<p>parser second</p>') </script>
<p>
parser third
</p>
</body>
</html>
复制代码
能够看到document.write调用的输出要在脚本以后的标签的前面。
Tokenizer在编译器领域是比较常见的一个名词,直译过来就是令牌化的意思,咱们考虑一下HTML文档中有多少种类型的字符:
而咱们接收到的是一串无状态的字符串,为了方便HTML解析,咱们须要将这一长串字符串,切分红一系列子串,并打上相应的标签,赋予对应的状态,一个个的传递给Tree Construction,这就是Tokenizer的职责。
Tree Construction有一系列的插入状态,确保node节点插入在合适的位置,若是一个节点出如今非法位置则会致使Parser Error(eg:在head内写了个span标签),Parser Error不必定会致使Parser终止,规范定义了一系列纠错机制。
<!-- 一系列的插入状态保证了html解析成dom能生成以下的结构 -->
<!DOCTYPE html>
<html lang="en">
<head></head>
<body></body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 非法的节点类型,会致使Parser Error,根据纠错机制会忽略该节点。 document.querySelector('#invalid')将会是null -->
<span id="invalid">2</span>
</head>
<body></body>
</html>
复制代码
Tree Construction最终会产出一个node节点并插入到Dom中,这时候咱们就能够经过JavaScript去操做Dom了。
这样就完事了?好戏如今才开始! 在刚刚的叙述过程当中,隐藏了对具体标签的解析方式,<script src="index.js" />和<link href="index.css" />怎么能同样呢?
接下来咱们说说样式、脚本在HTML Parser执行过程当中的具体表现。
坊间流传的最广的说法就是,样式加载不会阻塞HTML解析,而脚本会。
这句话太过模糊,毕竟样式有内联样式、外部样式等,一样脚本也有内联脚本、外部脚本,而外部脚本又有defer、async这些属性进行再次区分。
样式的解析并不属于HTML Parser的工做内容,对于HTML Parser而言只须要把link或者style标签插入到Dom中就完事了,因此对于style和link标签内的样式资源的加载和解析工做是经过并行的方式去执行的。
所谓的并行就是将主线程(HTML Parser所在的线程)返回,并建立一个子线程(或其余并行的实现方式eg:fiber),将接下来的任务(eg: 下载样式和解析样式)放到子线程中执行。
到此为止,结论都指向样式加载解析不阻塞HTML解析,大部分状况下这个结论都是对的,说到这就要说下解析过程当中的一个全局变量script-blocking style sheet counter。
这个变量会在以下场景发生变化:
那么什么样的样式资源叫script-blocking style sheet呢?
// script-blocking style sheet
<link rel="stylesheet" type="text/css" href="./index.css"/>
<style> @import './index2.css'; </style>
复制代码
如今只须要记住这个变量和脚本执行还有渲染有关系便可,具体联系咱们接着看脚本的加载模式。
这是一张流传比较广的script加载流程图,基本涵盖了大部分script加载对HTML Parser的影响,但还有部分细节的缺失,咱们看看规范是怎么定义的。
对于没有defer/async属性的script,咱们称之为pending parsing-blocking script,但须要注意的是:
那么pending parsing-blocking script是如何加载执行的呢?
经过上面步骤,咱们看到在这个场景下,样式的加载解析是会阻塞pending parsing-blocking script的执行的,进而致使HTML Parser的阻塞。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<!--在我解析完成以前,pending parsing-blocking script别想执行-->
<link rel="stylesheet" type="text/css" href="./index.css?lazy=1000" />
</head>
<body>
<!--我是pending parsing-blocking script,我会阻塞Parser, 但我要等到样式解析完成后才能执行-->
<script src="./index.js"></script>
<!--我要等到👆的脚本执行完成后才能解析-->
<span>hello</span>
</body>
</html>
复制代码
为何要样式解析要设计成阻塞这部分脚本执行的呢?考虑以下场景:
// index.css
.color {
color: red
}
复制代码
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<link rel="stylesheet" type="text/css" href="index.css" />
</head>
<body>
<p class="color">个人颜色是什么</p>
<script> const element = document.querySelector('.class'); const color = window.getComputedStyle(element).color; console.log(color) // rgb(255, 0, 0); // 若是script没有等待css加载完毕就执行,会致使脚本获取到错误样式 </script>
</body>
</html>
复制代码
咱们知道script标签还有async和defer两种影响它加装执行的属性,咱们看看具体表现又是如何。须要知道的几点:
当HTML Parser遇到带有defer标识且没有async标识的script标签,HTML Parser会将对应的script存在一个名为 list of scripts that will execute when the document has finished parsing队列中(有序),而且进行并行下载(不会阻塞主线程),但不会执行;而是等到HTML Parsing完成后,再回头执行这个队列,具体执行时机咱们后面还会讲。
当HTML Parser遇到带async标识的script标签的时候,Parser依然会选择把它存起来先,存在一个名为set of scripts that will execute as soon as possible的集合中,而后开启并行下载,与defer不一样的是,当async script加载完成后,会马上寻找机会执行(event loop next tick);这样形成的结果是async script的运行时机不可预测,且是无序的(下载完的先执行);且当在HTML Parsing完成以前,async script下载完毕,依然会阻塞后续的Parser任务(可是async script下载期间不阻塞Parser)。
还有一种类型的script list名为list of scripts that will execute in order as soon as possible,目前我探索出来的仅有以下状况符合这种类型的脚本:
// 由JavaScript建立,且aysnc为false的script element
const script = document.createElement('script')
script.async = false; // javascript 建立的script标签async默认为true
script.src = 'dy.js';
document.body.append(script);
复制代码
对于这种类型的脚本,解析和执行形式和async相似,惟一不一样的是这种类型的脚本,会按照添加的顺序执行,而async script是无序的;另外这两种脚本的执行都是不会被样式表的解析所阻塞的。
到这里咱们应当还有如下疑问:
HTML Parser在完成解析工做后,还有一些事项须要收尾,好比触发一些事件,把没跑的脚本给跑了等等;须要留意的是HTML Parser解析工做完成后不表明样式表的解析工做完成了,毕竟解析样式表的工做不属于Parser,这意味着script-blocking style sheet counter有可能大于0。
一个前置知识document.readyState能够是三个值之一:lodaing、interactive、complete,加载文档时是loading,当状态发生改变时会触发document.onreadystatechange事件。
解析完HTML文本后Parser会执行如下步骤:
了解这些底层逻辑有助于咱们,在进行编译时优化时拥有方向,处理首屏加载问题上能够认识到是什么在阻塞浏览器的加载。
其实对于script而言,最优的加载方式早就写在各大论坛的各大博客上了,就是挂在HTML的最后,可是当咱们要作一些特殊操做的时候,忽然须要script不被放在头部的样式阻塞,或者并行加载一些依赖,这些知识就派上了用场。
说完这些,看完的同窗可能会发现文中频繁出现event loop和任务的字眼,其实本质上HTML Parser就是跑在event loop上的一个任务,其实event loop的逻辑在HTML Standard中的规范至关复杂,它不只仅是调度执行JavaScript的任务,它基本调度了页面中的全部任务。
个人下一篇文章应该会是根据HTML Standard的描述去解析event loop,其中会包括event loop调度模式,会包括原本中提到的任务暂停机制,还有你们应该感兴趣的渲染时机的问题,其实本文没有提到渲染的问题,但其实渲染可能出如今CSS加载完成后任意一个时机。
若是大伙以为写得还行,但愿能点个赞,对下一篇感兴趣能够加个关注😁