· 626 words · 4 min
HTML 是不可以被渲染引擎理解的,要将其转换为可理解的内部结构 DOM。DOM 有三个层面的左右:
渲染引擎内部的 HTML 解析器(HTMLParser)模块负责将 HTML 字节流转换为 DOM 结构。 HTML 是随着网络进程的加载开始解析的,加载了多少数据,解析器便解析多少数据。
网络传输过来的字节流转换为 DOM 需要三个阶段,首先分词器将字节流转换为 Token,二三阶段同时进行, 将 Token 解析为 DOM 节点,然后将 DOM 节点添加到 DOM 树中。HTMLParser 维护了一个 Token 栈结构,用来计算节点之间的父子关系。
一段稍微复杂点的 HTMl 文件:
<html>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
</script>
<div>test</div>
</body>
</html>
HTML 中间插入了一段 JS 脚本,解析到 script 标签时,HTML 会暂停 DOM 的解析, 因为接下来的 JS 可能要修改已经生成的 DOM 结构。脚本执行完后,HTMLParser 恢复解析,直至生成最终的 DOM。
出了内嵌的 script 之外,还有引入的 JS 文件。其解析执行时,首先需要下载这段 JS 代码, 下载过程会阻塞 DOM 解析,而通常下载很耗时,受网络环境、JS 文件大小限制。不过 Chrome 做了很多的优化, 主要优化为预解析操作,用来预先下载 JS、CSS 文件。
JS 会阻塞 DOM,不过仍有其他策略进行规避,比如使用 async
和 defer
来标记代码:
<!-- 下载完立即执行 -->
<script async type="text/javascript" src='foo.js'></script>
<!-- 下载完在 DOMContentLoaded 事件之前执行 -->
<script defer type="text/javascript" src='bar.js'></script>
包含外部 CSS 时:
<html>
<head>
<style src='theme.css'></style>
</head>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang' //需要DOM
div1.style.color = 'red' //需要CSSOM
</script>
<div>test</div>
</body>
</html>
代码中引入了 CSS 文件,在执行 JS 之前,需要等外部的 CSS 下载完成并解析生成 CSSOM 之后,才能执行 JS 脚本。 因此渲染引擎遇到 JS 脚本时,无论脚本是否操作了 CSSOM,都会执行 CSS 文件的下载解析操作,再执行 JS 脚本。 因此 JS 会阻塞 DOM 的生成,CSS 会阻塞 JS 的执行。