这里简单提一下 script 标签加载过程,具体的细节要放在整个浏览器渲染过程中来讲解。
我们知道,浏览器请求并解析一个页面的大致过程:
在这个过程中,我们需要知道 JavaScript 执行的两个细节:
也就是说当浏览器在解析 HTML 文档时,如果遇到 <script>
,便会停下对 HTML 文档的解析,转而去处理脚本。如果脚本是内联的,浏览器会先去执行这段内联的脚本,如果是外链的,那么先会去加载脚本,然后执行。在处理完脚本之后,浏览器便继续解析 HTML 文档。
当然,下文提及的 async 和 defer 会对这个过程有不同的影响。
因为 JavaScript 可以查询任意对象的样式,所以意味着在 CSS 解析完成后,JavaScript 才能操作。
在使用 <script>
的过程中,有两个属性是对脚本的加载顺序有着很大影响的:async 和 defer。
让我们看下面四种使用情况:
<script src="script.js"></script>
没有指定 async 和 defer,这种情况下浏览器会立即下载脚本并执行,这会打断 HTML parsing 过程。
指定了 defer 后,脚本的下载和执行都不会暂停 HTML parsing。
defer 脚本会在 HTML 解析完成后,DOMContentLoaded 事件之前执行,用下面的一段代码来实验:
// html
<body>
<div class="container">Hello</div>
<script src="./script.js" defer></script>
</body>
// script.js
function block() {
let start = +new Date;
let end = start + 10;
while(start <= end) {
start = +new Date;
}
}
window.addEventListener('DOMContentLoaded',() => {
block();
});
block()
并且它还按照脚本的位置顺序依次加载,long.js 就算比 small.js 慢加载,最后也会先执行:
<script defer src="long.js"></script>
<script defer src="small.js"></script>
<script async src="script.js"></script>
指定了 async 后,脚本的下载过程不会打断其他操作,只有在脚本执行阶段才会暂停 HTML parsing。
async 跟上面的 defer 相同的点在于,它们都不会阻塞 HTML 解析,但 async 会让脚本完全独立,它不管其他脚本的加载顺序,只要加载成功它就执行。
正是因为这样的特性,它跟 DOMContentLoaded 的关系就变得有些怪异了,有资料显示说, DOMContentLoaded 在 async 脚本执行之前之后都有触发的可能性,但我的实验结果告诉我:它根本就不会触发!!
<body>
<div class="container">Hello</div>
<script src="./script.js" async></script>
</body>
这个细节等以后碰到了再深挖,埋个坑。
有的时候我们需要动态加载 script 脚本:
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
动态脚本默认情况下会跟 async 属性是一样的,也就是说它没有先后顺序,如果需要保证多个动态脚本的先后顺序,我们需要手动设置 async = false:
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
// long.js runs first because of async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");
由上面的讨论我们可以总结:
属性 | 顺序 | DOMContentLoaded | onLoad |
---|---|---|---|
async | 先加载先执行,与脚本位置无关 | 不相关 | 在事件之前执行 |
defer | 按脚本的位置顺序 | 在事件之前执行脚本 | 在事件之前执行 |
至于在什么样的场景下使用这些属性:
在真实的浏览器环境下,由于异步网络情况,加载的脚本并不一定按照指定的顺序执行,对互相有依赖的脚本有一定风险。为了保证 HTML 渲染,最好的方式还是将 script 脚本放在 body 下面。
以及 async 和 defer 都不能保证一定不会中断 HTML 渲染,所以得确保脚本内容在 onLoad 事件之后才开始运行。
这里再多谈几句 DOMContentLoaded 与 load 事件的区别。
简单看图,了解一下什么是 DOMContentLoaded 与 load:
当 HTML 文档解析完成就会触发 DOMContentLoaded,而所有资源(比如图片、样式)加载完成之后,load 事件才会被触发。
load 事件要比 DOMContentLoaded 晚一点触发。而上面提到的 async 和 defer 属性都会阻塞 load 事件。
点击这个测试网页 也可以看到两者的直观区别。
由上面的话题,引申出另外的一个问题,动态引入样式表会不会影响浏览器渲染?
结论是:
具体探究过程见参考后两篇文章。需要注意的是,如果动态插入的外部样式表在 HTML 解析后才加载完成,是会造成 FOUC 问题,因为前次的 HTML 文档已经显示出来了,样式资源后续才解析渲染,这会造成一闪而过的样式变化。
这些都是小的知识点细节,把它放在整个的浏览器渲染流程角度来思考,才能彻底把握这背后的原因。