浏览器解析 HTML 文件
这是一道比较经典的面试题,我们可以来探讨它。
基本步骤
(1)按照 HTML 文件从上往下的顺序解析,浏览器先解析 head 标签内的代码;
见到 style 标签直接解析;
见到 link 标签就立刻下载其引入的 CSS 文件,开启下载后继续去解析下方代码(解析的行为不会因为有文件在下载而暂停);
见到 script 标签,浏览器暂停解析(如果上方有下载任务不会暂停,只是代码的解析暂停),浏览器将控制权交给 JS 引擎,如果script 标签内引用了外部脚本,就下载该脚本,不然就直接执行 JS 代码。执行完毕后将控制权交给浏览器渲染引擎;
注意:如果上方有正在下载的 CSS 文件或者 JS 脚本,会等该文件下载后再执行 JS 代码。
(2)接着解析 body 标签内的代码;
head 标签中如果有文件还没下载完毕,会继续下载;
因为 head 标签中的 style 标签已声明了一部分样式,所以浏览器解析时会按照指定的样式去解析;
body 标签内若引入了 script 标签,同样地会先让 JS 引擎去加载脚本或执行里面的 JS 代码。如果script 标签内要获取的 DOM 的位置在此标签位置以下,此时获取到的是 undefined。。可以借助 wiondow.onload() 事件。该事件会在整个页面加载完毕后执行,就可以拿到任意位置的 DOM。
补充:
onload() 事件和 DOMContentLoaded() 事件的区别:
(1)onload事件是在页面所有的 DOM,样式表,脚本,图片等资源加载完毕之后触发;
(2)DOMContentLoaded事件是 DOM 加载完就触发,样式表,脚本等其他资源不管。
(3)body 代码执行完毕,等待该页面的所有 CSS 文件加载完毕后,CSS 会重新渲染整个页面的 HTML 元素。
script 标签的 async defer属性(面试高频考点)
上面提到,浏览器解析 HTML 文件时,默认情况下,遇到 JS 代码会直接执行。(由拿不到下面的 DOM 也可以证实这一点)那也就是说如果该 script 标签内的脚本内容过多,在执行大量 JS 代码的时候,阻塞 HTML 文件继续解析的现象会更明显。
script 标签有多个可选属性,其中跟 HTML 文件解析息息相关的有
async和defer,这两个属性都是只对外部脚本文件(通过 src 属性引入的脚本文件)有效。对于内嵌的脚本,依然直接执行。
(1)async:表示应该立即开始下载脚本,但不能阻碍其他资源的加载。下载完成的那一刻立即执行代码。
假设页面中有三个 script 标签使用了 async 属性,引入顺序分别是标签A,标签B,标签C,会按 ABC 的顺序去加载,此过程继续解析标签 C 下面的代码,不会阻塞。然后 ABC 中哪一个脚本文件先下载完,就立刻执行该文件的代码,并不会按声明的顺序来执行。
(2)defer:表示脚本可以 延迟 到文档完全被解析或显示之后(一般是 DOMContentLoaded 事件触发之前执行)再 执行。注意:只是延迟执行,该脚本的下载不会被延迟。
假设页面中有三个 script 标签使用了 defer 属性,引入顺序分别是标签A,标签B,标签C,当 DOMContentLoaded 事件触发之前,会按引入顺序依次执行 ABC 中的代码,不管 ABC 哪一个文件先下载完。
(3)如果是动态加载脚本的方式,就是使用 document.createElement() 的方式创建一个 script 标签,并且添加 src 属性引入外部文件的加载方式。这种情况默认是 async 的情况。
总结:两者最大的区别就是:
JS 代码执行的时机。
默认情况下,遇到 JS 代码就立刻执行它,会阻塞 HTML 文件的解析;
使用了 async 不会阻塞,JS 代码在脚本加载完毕后立即执行;
使用了 defer 不会阻塞,JS 代码在整个页面的 DOM 加载完毕之后,DOMContentLoaded 事件之前执行。
浏览器渲染的大致过程
- 解析 HTML 文件,生成 DOM 树;
- 解析 CSS 文件生成 CSSOM 树;
- 将 DOM 树 和 CSSOM 树结合,生成 Render树,渲染到页面上。
总结
(1)因为 DOM 解析和 CSS 解析是两个并行的过程,所以CSS 文件加载不会阻塞 DOM 树的解析;在开启 CSS 文件的下载后,会接着解析后面的代码;在渲染工作开始之前,浏览器会先解析出一棵完整的 DOM 树,等待最终的 CSS 样式确定下来再一次性渲染。
(2)因为渲染树 Render树生成需要有 DOM 树和 CSSOM 树,所以浏览器渲染之前需要拿到完整的 CSSOM 树,因此必须等待 CSS 文件加载完毕。因此CSS 加载会阻塞 DOM 树的渲染;
其实这个问题还是很容易想到的。CSS 加载完毕后样式不一定是最初定义的样式,如果不阻塞渲染的话,那么要多次渲染,性能低下。举例如下:
比如 style 标签中声明 div 颜色为粉色,下方又有 link 标签将该 div 声明为蓝色。如果没有阻塞渲染,单是这个 div 就要让页面渲染2次,更不要说还有其他那么多 HTML 元素。
因此,浏览器就干脆等页面所有元素的最终样式确定下来之后,一次性渲染,这样会大大减少回流重绘的次数,性能更佳。
(3)CSS 加载会阻塞下方 JS 语句的执行;
这个也是可以比较容易想到的。JS 代码中很有可能会对元素的 DOM 样式进行修改,所以浏览器先把样式表加载完毕,然后再让 JS 引擎执行 JS代码,执行的过程当中如果有对样式做修改,会把这个修改作用到 CSSOM 树和 Render 树中,再渲染到页面上。
如果下方的 JS 代码先执行,假如里面有修改元素的样式,那么 CSS 文件解析完毕之后可能会把 JS 修改的样式覆盖掉。
案例
看看下面这个 HTML 文件是如何解析的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
/* 内部 CSS 代码 A */
</style>
<!-- 外部 CSS 文件 B -->
<link rel="stylesheet" href="" />
<script>
// 内部 JS 代码 C
</script>
<!-- 外部 JS 文件 D -->
<script src=""> </script>
</head>
<body>
<!-- 这里有很多 DOM -->
</body>
</html>
浏览器从上往下:
(1)解析代码A;
(2)遇到外部 CSS 文件B,立刻加载B ,但不会被阻塞,下载B的过程中会继续往下解析;
(3)遇到内嵌的 JS 代码C,浏览器将控制权交给 JS 引擎,JS 引擎直接执行代码 C,此时 HTML 文件的解析暂停。当执行完代码C之后,再继续往下;
(4)遇到外嵌脚本文件D,默认情况下会去加载D,并执行D,该期间,浏览器不会继续往下解析 HTML 文件,当代码D执行完毕之后,再继续往下解析 body 中的 HTML 代码。
如果是添加了属性 async,则立刻下载D,但下载D的同时继续往下解析,不会阻塞;下载完毕立刻执行代码,不管此时页面解析到何处;
如果是添加了属性 defer,则立刻下载D,同样地,下载D的同时继续往下解析,不会阻塞;下载完毕后等待页面所有的 DOM 都被解析完毕了才会执行。