两个问题:

  • 在解析过程中遇到 JS 脚本,DOM 解析器如何处理?
  • DOM 解析器如何处理跨站点资源?

    什么是 DOM?

  • 从页面视角来看:DOM 是生成页面的基础的数据结构

  • 从 JS 脚本视角来看:DOM 是提供给 JS 脚本操作的接口。
  • 从安全角度来看:DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被过滤掉了。

DOM 树如何生成?

在渲染引擎内部,有一个 HTML 解析器,负责将 HTML 字节流转为 DOM 结构。

  • HTML 解析器是如何工作的?
    HTML 解析器并不是等整个文档加载完之后才去解析,而是网络进程加载多少数据,HTML 解析器就解析多少数据。
    详细流程如下:
    网络进程接收到响应头后,根据 content-type 字段判断文件类型,若是 text/html,浏览器会认为是一个 html 文件并为其选择或创建一个渲染进程。渲染进程准备好后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后往管道里放,渲染进程则在管道的另一端不断读取数据,同时将数据传给 HTML 解析器,HTML 解析器会动态接受字节流并将其解析为 DOM。
  • 字节流转换为 DOM 示意图

字节流转换为 DOM.png

  1. 通过分词器将字节流转换为 Token。

生成的 Token 示意图.png

  1. 将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。
    HTML 解析器维护一个栈结构,用来计算节点间父子关系,文本 Token 不需要压入栈中。通过分词器产生的 Token 不停的压栈和出栈,整个解析过程就这样一直持续下去,直到分词完成。
    HTML 解析器会为 StartTag Token 创建一个 DOM 节点加入到 DOM 树中。若解析出来是是 EndTag Token, HTML 解析器会查看栈顶是否是对应的 StartTag Token. 若是,则弹出,该元素解析完成。

1.png
2.png
3.png
4.png
5.png

JS 如何影响 DOM 生成?

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <body>
  4. <div>1</div>
  5. <script>
  6. let div1 = document.getElementsByTagName('div')[0]
  7. div1.innerText = 'time.geekbang'
  8. </script>
  9. <div>test</div>
  10. </body>
  11. </html>

解析到 script 标签时,渲染引擎判断是脚本,就会暂停 DOM 解析,因为 JS 可能会修改已生成的 DOM。

  • 若换成下面这种
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <body>
  4. <div>1</div>
  5. <script src="foo.js"></script>
  6. <div>test</div>
  7. </body>
  8. </html>

与上面解析过程类似,唯一不同在于要先下载 JS,下载过程会阻塞 DOM 解析。

  • JS 操纵 CSSOM
  1. //theme.css
  2. div {color: red;}
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <body>
  4. <div>1</div>
  5. <script>
  6. let div1 = ...
  7. div1.style.color = 'red' // 需要 CSSOM
  8. </script>
  9. <div>test</div>
  10. </body>
  11. </html>

JS 内部操作了 CSSOM,所以要等外部 CSS 文件下载并解析完成后才能执行。而 JS 引擎在执行脚本前是不知道 JS 是否操纵了 CSSOM,所以渲染引擎遇到 JS 脚本时,不管是否操作 CSSOM,都会执行 CSS 的下载、解析操作,然后再执行 JS 脚本。

JS 会阻塞 DOM 生成,样式文件会阻塞 JS 执行。

渲染引擎内部有一个安全检查模块XSSAuditor用来检测词法安全,比如是否引用外部脚本,是否符合 CSP(Content Secure Policy) 规范,是否存在跨站点请求等;若不符合,XSSAuditor 会拦截该脚本。

预解析操作

Chrome 内部做了优化,当渲染引擎接收到字节流后,会开启一个预解析线程,分析 HTML 文件中包含的 JS、CSS 文件,解析到后,预解析线程会提前下载这些文件。

异步加载脚本

若 JS 文件中没有操作 DOM 的相关代码,就可以将脚本设为异步加载。

  • async: 异步加载脚本,一旦加载完成,立即执行,无 src 属性时无效。
  • defer: 异步加载脚本,加载完成后,会在 DOMContentLoaded 事件之前执行,无 src 属性时无效。