文章目录
- 1 JS 延迟加载
- 2 JS 延迟加载方式
- 2.1 defer
- 2.2 async
- 2.3 defer 和 async 异同点
- 2.4 setTimeout
- 2.5 动态创建 DOM 方式
- 2.6 将脚本元素放在文档的底部
- 3 DOMContentLoaded
- 4 load
1 JS 延迟加载
『什么是 JS 延迟加载』
简单来说,JS 延迟加载就是等 HTML 文档解析完成后再执行 JS 脚本。
『JS 延迟加载有什么作用』
1️⃣ 有助于提高页面加载速度。因为外联普通 JavaScript 脚本的加载和执行都会阻塞 HTML 文档的解析,从而影响了页面加载速度。
2️⃣ 确保 JS 脚本能正确操作 DOM 对象。由于 JS 脚本执行时,HTML 的解析还未完全结束,可能会无法获取到对应的 DOM 对象,此时会返回 null
。
// index.js
const firstEl = document.getElementById('first');
const secodeEl = document.getElementById('secode');
console.log(firstEl); // <div id="first"></div>
console.log(secodeEl); // null
<body>
<div id="first"></div>
<script src="index.js"></script>
<div id="secode"></div>
</body>
『JS 延迟加载有哪些方式』
-
defer
属性 -
async
属性 - 动态创建 DOM 方式
- 使用
setTImeout
延迟方法 - 让 JS 最后加载
为了更好了解各个方式的 JS 脚本执行时机,先看一下外联普通 JavaScript 执行过程:
// 普通外联 JavaScript
<script src="index.js"></script>
遇到此类 <script>
标签会暂停解析 HTML 文档,先请求并执行 JS 脚本后,再继续解析 HTML 文档。
从这幅图可以解释上前面例子 firstEl
为什么会返回对应的 DOM 对象,而 secodeEl
会返回 null
。因为执行 JS 脚本时,<div id="first"></div>
已经被解析,所以 DOM 树中存在该节点,而 <div id="secode"></div>
还未被解析,DOM 树中自然就不存在该节点,所以此时就会返回 null
。
2 JS 延迟加载方式
2.1 defer
『defer』
defer
这个布尔属性被设定用来通知浏览器该脚本将在 HTML 文档完成解析后,触发 DOMContentLoaded
事件前执行。— MDN
『加载过程』
外联 defer
JS 脚本的加载过程:HTML 文档解析过程中遇到此类 <script>
标签不阻塞 HTML 文档的解析,而是会暂存到一个队列中,等整个 HTML 文档解析完成后再按队列的顺序请求并执行 JS 脚本。
『🌰 例子』
const firstEl = document.getElementById('first');
const secodeEl = document.getElementById('secode');
console.log(firstEl);
console.log(secodeEl);
<body>
<script>
document.addEventListener("DOMContentLoaded", function () {
console.log("DOMContentLoaded");
});
</script>
<div id="first"></div>
<script defer src="index.js"></script>
<div id="secode"></div>
</body>
可以看到,不管是 firstEl
还是 secodeEl
都返回了对应的 DOM 对象,所以 JS 脚本是在 HTML 文档解析完成之后才执行的,另外,JS 脚本是执行在 DOMContentLoaded
事件之前的。
『注意点』
1️⃣ HTML5 规范要求脚本按照它们出现的先后顺序下载和执行。
// a.js
console.log('a');
// b.js
console.log('b');
// c.js
console.log('c');
<head>
<script>
document.addEventListener("DOMContentLoaded", function () {
console.log("DOMContentLoaded");
});
</script>
<script defer src="a.js"></script>
<script defer src="b.js"></script>
<script defer src="c.js"></script>
</head>
“实际中,延迟脚本并不一定会按照顺序执行。” 这句话在一些文章看到,可我运行代码发现,是按照顺序执行的。不清楚怎么回事?先记录下来吧。
2️⃣ defer
只适用于外联脚本,对内联脚本不起作用。
<head>
<script defer>
const firstEl = document.getElementById("first");
const secodeEl = document.getElementById("secode");
console.log(firstEl);
console.log(secodeEl);
</script>
</head>
<body>
<div id="first"></div>
<div id="secode"></div>
</body>
3️⃣ 有 defer
属性的脚本会阻止 DOMContentLoaded
事件,直到脚本被加载并且执行完成后才会触发 DOMContentLoaded
事件。
从注意点 1️⃣ 的例子中可以看到带有 defer
的 JS 脚本全部执行完才会触发 DOMContentLoaded
事件。
2.2 async
『async』
对于脚本来说,如果存在 async
属性,那么普通脚本会被并行请求,并尽快解析和执行。
『过程』
外联 async
JS 脚本的加载过程:遇到此类 <script>
标签会并行下载脚本,不会阻塞 HTML 的解析。如果下载完成后 HTML 还未解析完成,则会暂停解析 HTML ,先执行 JS 脚本后,再解析 HTML。
但是如果 HTML 已经解析完毕,外联 async JS 脚本还未下载完成,则过程如下:
『🌰 例子』
// a.js
const aEl = document.getElementById('a')
console.log(aEl);
// b.js
const bEl = document.getElementById('b')
console.log(bEl);
// c.js
const cEl = document.getElementById('c')
console.log(cEl);
<head>
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('DOMContentLoaded');
})
window.addEventListener('load', function () {
console.log('load');
})
</script>
<script src="a.js" async></script>
<script src="b.js" async></script>
<script src="c.js" async></script>
</head>
<body>
<div id="a"></div>
<div id="b"></div>
<div id="c"></div>
</body>
可以看到,三个 JS 脚本会按顺序执行(如果 a.js
下载的时间久一点,就不一定会执行在 b.js
前面),但实际上,有 async
属性的 JS 脚本的执行顺序是无法预测的。并且也无法确保与DOMContentLoaded
的执行先后顺序。
『注意点』
1️⃣ 只适用于外联脚本,这一点和 defer
一致。
2️⃣ 如果有多个声明了 async
的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序。
3️⃣ async
会在 load
事件之前执行,但并不能确保与 DOMContentLoaded
的执行先后顺序。
2.3 defer 和 async 异同点
相同点:
- 两者都只使用与外联脚本,对内联脚本不起作用。
不同点:
- 多个
defer
JS 脚本会按顺序下载和执行 - 多个
async
JS 脚本不会按顺序下载和执行。 -
defer
JS 脚本执行在 DOMContentLoaded 事件之前。 -
async
JS 脚本不能确保与DOMContentLoaded
事件的执行先后顺序。
2.4 setTimeout
延迟执行 JS 代码,给网页加载留出更多时间。
『过程』
『🌰 例子』
<script>
function getElement() {
const firstEl = document.getElementById("first");
const secodeEl = document.getElementById("secode");
console.log(firstEl); // <div id="first"></div>
console.log(secodeEl); // <div id="secode"></div>
}
// 延迟 2s 后再执行
setTimeout(getElement, 2000);
</script>
2.5 动态创建 DOM 方式
『过程』
『🌰 例子』
<body>
<div id="first"></div>
<div id="secode"></div>
<script>
function downloadJSAtOnload() {
let el = document.createElement("script");
el.src = "index.js";
document.body.appendChild(el);
}
if (window.addEventListener)
window.addEventListener("load", downloadJSAtOnload, false);
else if (window.attachEvent)
window.attachEvent("onload", downloadJSAtOnload);
else window.onload = downloadJSAtOnload;
</script>
</body>
2.6 将脚本元素放在文档的底部
『加载过程』
『🌰 例子』
<body>
<div id="first"></div>
<div id="secode"></div>
<script src="index.js"></script>
</body>
3 DOMContentLoaded
当 HTML 文档被加载和解析完成后之后,DOMContentLoaded
事件就会被触发,无需等待样式表、图像和子框架的完全加载。
4 load
当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发 load
事件。
它与 DOMContentLoaded
不同,后者只要页面 HTML 解析完成之后就触发,无需等待依赖资源的加载。