欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

async 和 defer

程序员文章站 2022-04-24 15:14:04
...

HTML加载过程

1. 两个引擎

浏览器的引擎可以分为渲染引擎和 JS 引擎。 JS 引擎相对独立,而渲染引擎又包括 HTML 解释器、 CSS 解释器、布局、图形、视频、图片解码器等。

JS 引擎独立于渲染引擎,而浏览器加载网页需要 JS 引擎和渲染引擎相互协作。一般情况下,当 HTML 解释器遇到 <script> 标签时,浏览器会将控制权交给 JS 引擎,JS 引擎对内联的代码会直接执行,对外部的 JS 文件需要先下载再执行。当 JS 引擎执行完毕,浏览器又会将控制权交给渲染引擎,继续构建 CSSOM 和 DOM。

但这种写作方式显然有问题,当下载 JS 文件时,会阻塞渲染引擎的工作导致页面加载的延迟,所以才有了 async 和 defer。

HTML文档解析的三种情况

就 JS 同步执行而言,HTML 文档的解析会遇到三种情况:

  • HTML

此时,渲染引擎会直接将 HTML 元素解析成 DOM 树然后进行渲染等操作,在 DOM 树生成的同时触发 DOMContentLoaded 事件。

async 和 defer

  • HTML+CSS

此时,仍然会将 HTML 元素解析成 DOM树,并在 DOM 树生成的同时触发 DOMContentLoaded 事件,但是不同的是,渲染树的生成在有 CSS 参与时,是通过 DOM + CSSOM 共同来渲染的,所以渲染过程的开始是在 DOM 和 CSSOM 都生成完成之后。

async 和 defer

  • HTML+CSS+JS

同步的情况下,解析 HTML 元素时,如果遇到<script>标签,会有两种操作:获取 JS 代码、执行 JS 代码。如果此时正在生成 CSSOM,那么获取 JS 代码的操作可以和 CSSOM 的构建同时执行,但是只有菜 CSSOM 构建完成之后才能执行 JS 代码。如果<script>标签先于 CSS,那么就不存在等待 CSSOM 的过程。

**获取 JS 代码:**获取操作可以和 CSSOM 的构建同时执行。如果是内联 JS ,则不存在下载的操作,就只有执行操作,如果是外部 JS 文件,那么此时会去下载 JS 文件。
**执行 JS 代码:**需要等待 CSSOM 构建完成之后才可以执行。

async 和 defer

首屏时间

首屏时间可以理解为 DOMContentLoaded 事件的触发时间。另外,JS 代码写在 HTML 文件的头部和尾部(前)对首屏时间没有影响。其作用是确保 JS 代码执行时, DOM 和 CSSOM树已经解析完毕,在获取元素时不会出现获取为 null 的情况。

普通加载

普通加载就是上文所介绍的同步加载,不再赘述。

写法:

<script src="javascript.js"></script>

async

意义:异步加载 JS 文件,加载完成后直接执行。

写法:

<script src="javascript.js" async></script>

既然只是异步加载,那么加载完成后可能的结果:

  • HTML 文档解析完毕
  • HTML 文档未解析完毕

其实大部分情况下,async 加载的 JS 脚本会在 HTML 解析完毕之后执行。因为涉及到下载,而 HTML 是直接解析当前的 HTML 文件,所以如果 HTML 和 CSS 中的元素太多太复杂,导致 CSSOM 和 DOM 的解析时间过长,这个时候就会出现 JS 文件下载完毕准备执行时,HTML 文档仍然没有解析完毕。

所以,大部分情况下,不想阻塞 HTML 的解析时,使用 async 就够用了,如果涉及到 JS 脚本的执行顺序或者是为了更加严谨起见,可以使用 defer。

defer

意义:异步加载 JS 文件,全部 JS 文件加载完毕后,且 HTML 文档解析完毕之后,按顺序执行 JS 文件,最后触发 DOMContentLoaded 事件。

写法:

<script src="javascript.js" defer></script>

过程如下:
async 和 defer

这里有几个点需要注意:

  • 执行 JS 文件是按顺序执行的(特殊情况除外,后文有讲)
  • 执行 JS 文件是在 HTML 文档解析完毕之后,但是实在触发 DOMContentLoaded 事件之前
  • 同样是异步加载 JS 文件,不会阻塞 HTML 文档的解析

验证

验证:
HTML 代码如下:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <script src="script.js" async></script>

    </head>
    <body>
        <script>
            document.addEventListener("DOMContentLoaded", function() {
                console.log("DOMContentLoaded");
            });
        </script>

        <div>
          <div class="box">盒子1</div>
          此处省略 n 个 div
          <div class="last">盒子2</div>
        </div>
    </body>
</html>

JS 代码如下:

console.log("script.js运行")
var lastBox = document.querySelector(".last")
console.log(lastBox)

运行情况如下:

  1. 当 div 个数较少时,打印结果总是如下:
DOMContentLoaded
script.js运行
<div class=​"last">​盒子2​</div>​
  1. 当 div 个数逐渐增加到临界值时,打印结果:
script.js运行
<div class=​"last">​盒子2​</div>​
DOMContentLoaded

或者是:

script.js运行
null
DOMContentLoaded
  1. 当 div 超过临界值时,结果总是:
script.js运行
null
DOMContentLoaded

当修改成 defer 之后,无论 div 个数为多少,结果总是:

script.js运行
<div class=​"last">​盒子2​</div>​
DOMContentLoaded

defer不按顺序执行

有时候即使写了 defer 也不会按照顺序执行,但是多个 JS 文件之前又确实存在先后的依赖关系。此时的解决方案是只写一个 defer ,也就是只包含一个延迟脚本。解决方法又两个,一个是写到一个 JS 文件中,使用 defer 加载这个 JS 文件。另外一个方法就是依赖文件使用同步的方式加载,后执行的 JS 文件使用 defer 异步加载。

如:

<script src="first.js" defer></script>
<script src="seconde.js" defer></script>

改成:

<script src="first.js"></script>
<script src="seconde.js" defer></script>
相关标签: Web开发