script
标签用于加载脚本与执行脚本,在前端开发中可以说是非常重要的标签了。
直接使用script
脚本的话,html
会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的DOM
渲染。
现在大家习惯于在页面中引用各种的第三方脚本,如果第三方服务商出现了一些小问题,比如延迟之类的,就会使得页面白屏;
好在script提供了两种方式来解决上述问题,async
和 defer
,这两个属性使得 script
都不会阻塞 DOM
的渲染;
但既然会存在两个属性,那么就说明,这两个属性之间肯定是有差异的。
1.defer
如果 script 标签设置了该属性,则浏览器会异步的下载
该文件并且不会影响到后续 DOM 的渲染
;
如果有多个设置了 defer 的 script 标签存在,则会按照顺序执行
所有的 script;
defer脚本会在文档渲染完毕后,DOMContentLoaded 事件调用前执行。
服务端配置 Server.js
const http = require("http");
const fs = require("fs");
let respondResourceToClient = (req, res) => {
fs.createReadStream(req.url.replace(/^\//, "")).pipe(res);
};
let server = http.createServer((req, res) => {
let extension = req.url.split("/")[req.url.split("/").length - 1];
let delay = (time) => {
setTimeout(() => {
respondResourceToClient(req, res);
}, time || 0);
};
if (extension === "index.html" || extension === "css") {
delay(0);
} else if (extension === "1.js") {
delay(1000);
} else if (extension === "2.js") {
delay(2000);
} else {
res.end("");
}
});
server.listen(1314);
console.log("listening at port 1314...");
我们做了一个测试页面,页面中包含了两个 script 标签的加载,给他们都加上 defer 标识。
PS.为了更直观,我们给 1.js 添加了 1s 的延迟,给 2.js 添加了 2s 的延迟。
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>星宿君</title>
</head>
<body>
<!-- console.log("2.js"); 2s延迟加载 -->
<script defer type="text/javascript" src="./js/2.js"></script>
<!-- console.log("1.js"); 1s延迟加载 -->
<script defer type="text/javascript" src="./js/1.js"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
console.log('dom content loaded, ready state:', this.readyState);
}, false);
window.addEventListener('load', function() {
console.log('window loaded, dom ready state:', document.readyState);
}, false);
</script>
</body>
</html>
下图是页面加载的过程 script 脚本的输出顺序;
不难看出,虽然 1.js 加载用时虽然比 2.js 短,但因为 defer 的限制,所以 Ta 只能等前边的脚本执行完毕后才能执行。
2.async
async 的设置,会使得 script 脚本异步的加载并在允许的情况下执行;
async 的执行,并不会按着 script 在页面中的顺序来执行,而是谁先加载完谁执行。
我们修改测试页面如下:
<!-- console.log("2.js"); 2s延迟加载 -->
<script async type="text/javascript" src="./js/2.js"></script>
<!-- console.log("1.js"); 1s延迟加载 -->
<script async type="text/javascript" src="./js/1.js"></script>
遂得到了如下的结果,页面加载时长上,并没有什么变化,毕竟都是异步加载的脚本。
但是我们可以看到一个小细节,DOMContentLoaded事件的触发并不受async脚本加载的影响,在脚本加载完之前,就已经触发了DOMContentLoaded。
其次,如果给async一定的时间,是有可能在DOMContentLoaded事件之前就执行的。
3.对比
普通script
文档解析的过程中,如果遇到 script 脚本,就会停止页面的解析进行下载(但是 Chrome 会做一个优化,如果遇到 script 脚本,会快速的查看后边有没有需要下载其他资源的,如果有的话,会先下载那些资源,然后再进行下载 script 所对应的资源,这样能够节省一部分下载的时间)。
资源的下载是在解析过程中进行的,虽说script1脚本会很快的加载完毕,但是他前边的script2并没有加载&执行,所以他只能处于一个挂起的状态,等待script2执行完毕后再执行。
当这两个脚本都执行完毕后,才会继续解析页面。
defer
文档解析时,遇到设置了 defer 的脚本,就会在后台进行下载,但是并不会阻止文档的渲染,当页面解析&渲染完毕后。
会等到所有的 defer 脚本加载完毕并按照顺序执行,执行完毕后会触发 DOMContentLoaded 事件。
async
async 脚本会在加载完毕后执行;
async 脚本的加载不计入 DOMContentLoaded 事件统计,也就是说下图两种情况都是有可能发生的。
顺序 | DOMContentLoaded | 方式 | |
---|---|---|---|
async | 加载优先顺序,脚本在文档中的顺序不重要——优先加载完成执行 | 不相关。可能在文档加载完成前加载并执行完毕。如果脚本很小或者来自于缓存,同时文档足够长,就会发生这种情况。 | 异步 |
defer | 文档顺序(它们在文档中顺序执行) | 在文档加载和解析完成之后(如果需要,则会等待),即在DOMContentLoaded之前执行。 | 异步 |
4.推荐的应用场景
defer
- 如果你的脚本代码依赖于页面中的 DOM 元素(文档是否解析完毕),或者被其他脚本文件依赖。
例:- 评论框
- 代码语法高亮
- polyfill.js
async
- 如果你的脚本并不关心页面中的 DOM 元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据。
例:- 百度统计
- 如果不太能确定的话,用defer总是会比async稳定。。。
Comments NOTHING