JavaScript加载顺序

我们知道HTML中可以使用<script>标签引入JavaScript代码,在现代的前端开发中这些JavaScript代码通常以模块化的机制进行整体加载,但在古早的开发模式中,可能需要引入多段JavaScript代码,这就不得不关注JavaScript的加载和执行顺序问题。

静态引入JavaScript

假如我们有3个JavaScript文件,1.js2.js3.js,其代码分别输出1、2、3三个数字,我们编写以下HTML代码引入这些JavaScript文件。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo</title>
    <script src="1.js"></script>
    <script src="2.js"></script>
    <script src="3.js"></script>
</head>

<body></body>

</html>

实际上,上面页面无论怎样刷新,1.js2.js3.js都会严格按照顺序加载并执行。在HTML中,DOM是从上到下解析的,上面的页面会在解析到<script src="1.js"></script>时,同步阻塞式的加载该文件并执行其中的代码,然后再继续解析,加载2.js并执行其中的代码,然后是3.js,最后才是接下来的<body>内容。

我们可以发现,上面代码会先顺序加载JavaScript文件并执行,这阻塞了页面的加载,打开页面可能白屏时间较长,因此以下写法更为流行。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo</title>
</head>

<body>
    <!-- 页面正文 -->
    <script src="1.js"></script>
    <script src="2.js"></script>
    <script src="3.js"></script>
</body>

</html>

HTML中我们将<script>放在了<body>的最后,这样页面内容可以先渲染出来,然后再加载执行JavaScript,这样页面的白屏时间更短,用户体验更好。

defer引入JavaScript

实际上,我们通常值关注Java的执行顺序,而JavaScript的加载过程一般没必要同步阻塞,我们可以为<script>设置defer属性。此时JavaScript的加载会和DOM解析并行执行,但defer会将JavaScript的执行安排到DOM解析完成后,以确保代码有正确的执行顺序。

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

如果使用defer引入多个JavaScript文件,它们的加载过程会和DOM解析并行,但会在DOM解析完成后顺序执行,这种写法既解决了并行加载问题,也不存在长时间白屏问题。

<script defer src="1.js"></script>
<script defer src="2.js"></script>
<script defer src="3.js"></script>

不过defer需要IE10版本及以上的浏览器才支持,因此兼容性不如直接将<script>写在<body>的最后。

async引入JavaScript

如果<script>设置async属性,则表示该JavaScript代码的加载过程和DOM解析是并行的,且加载完成后就立即执行。

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

但有一点要尤其注意,以之前的1.js2.js3.js为例,下面HTML代码中,1、2、3三个数字的输出顺序是随机的。如果这些文件之间有全局变量依赖关系就会造成随机的报错。

<script async src="1.js"></script>
<script async src="2.js"></script>
<script async src="3.js"></script>

动态引入JavaScript

除了上面我们编写的静态引入JavaScript的方式,我们还可能会通过JavaScript代码动态引入JavaScript。

var s1 = document.createElement('script');
s1.src = '1.js';
document.body.appendChild(s1);

这种引入JavaScript的方式看似没问题,实际却隐藏着深坑,因为我们动态创建的<script>标签默认是async的,也就是说如果你创建多个<script>标签,它们引入并执行的顺序完全是随机的,这在代码存在依赖时会造成难以定位的随机报错。

下面1.js2.js3.js代码的执行顺序是随机的。

var s1 = document.createElement('script');
s1.src = '1.js';
document.body.appendChild(s1);

var s2 = document.createElement('script');
s2.src = '2.js';
document.body.appendChild(s2);

var s3 = document.createElement('script');
s3.src = '3.js';
document.body.appendChild(s3);

解决这个问题的一种方式是使用onload回调函数,将三个JavaScript文件的加载改为串行。

var s1 = document.createElement('script');
s1.src = '1.js';
document.body.appendChild(s1);
s1.onload = function() {
    var s2 = document.createElement('script');
    s2.src = '2.js';
    document.body.appendChild(s2);
    s2.onload = function() {
        var s3 = document.createElement('script');
        s3.src = '3.js';
        document.body.appendChild(s3);
    }
};

更好的方法是将<script>标签的async属性设置为false,但defer设置为true,这样JavaScript会并行加载,节省网络加载的时间,但依然顺序执行。

var s1 = document.createElement('script');
s1.src = '1.js';
s1.async = false;
s1.defer = true;
document.body.appendChild(s1);
var s2 = document.createElement('script');
s2.src = '2.js';
s2.async = false;
s2.defer = true;
document.body.appendChild(s2);
var s3 = document.createElement('script');
s3.src = '3.js';
s3.async = false;
s3.defer = true;
document.body.appendChild(s3);

另一个常见的需求是想要在所有动态引入的JavaScript加载并执行完成后再执行剩下的操作,这可以通过设置onload回调函数实现。

s3.onload = function() {
    console.log('ok');
}
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap