我们知道HTML中可以使用<script>
标签引入JavaScript代码,在现代的前端开发中这些JavaScript代码通常以模块化的机制进行整体加载,但在古早的开发模式中,可能需要引入多段JavaScript代码,这就不得不关注JavaScript的加载和执行顺序问题。
假如我们有3个JavaScript文件,1.js
、2.js
、3.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.js
、2.js
、3.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,这样页面的白屏时间更短,用户体验更好。
实际上,我们通常值关注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>
的最后。
如果<script>
设置async
属性,则表示该JavaScript代码的加载过程和DOM解析是并行的,且加载完成后就立即执行。
<script async src="1.js"></script>
但有一点要尤其注意,以之前的1.js
、2.js
、3.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。
var s1 = document.createElement('script');
s1.src = '1.js';
document.body.appendChild(s1);
这种引入JavaScript的方式看似没问题,实际却隐藏着深坑,因为我们动态创建的<script>
标签默认是async
的,也就是说如果你创建多个<script>
标签,它们引入并执行的顺序完全是随机的,这在代码存在依赖时会造成难以定位的随机报错。
下面1.js
、2.js
、3.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');
}