脚本在页面的位置
script 通常被放在 header 或者 body 标签中,但位置的不同对于页面的加载效果也不一样。
放在 header 中
<head>
<title>script 加载机制</title>
<script src='/js/test1.js'></script>
<script src='/js/test2.js'></script>
<script src='/js/test3.js'></script>
</head>
你能看到 html 第一时间被加载进来,但页面 body 内容迟迟没有渲染出来。因为在等待 header 标签中 script 脚本的加载,3 秒后,整个页面渲染完成。
放在 body 底部
<body>
<h2>script 加载机制</h2>
<script src='/js/test1.js'></script>
<script src='/js/test2.js'></script>
<script src='/js/test3.js'></script>
</body>
这次 html 内容第一时间渲染完成,随后等待 js 的加载。
总结
脚本会阻塞页面的渲染,所以推荐将其放在 body 底部,因为当解析到 script 标签时,通常页面的大部分内容都已经渲染完成,让用户马上能看到一个非空白页面。
另外你能看到多个脚本之间都是异步向服务器请求,他们之间不互相依赖,最终只等待 3 秒,而非 3+3+3 秒。
脚本延迟时间不同会影响执行顺序吗?
一般我们都是按如下方式编写 script 脚本顺序的:
<script src="/js/test1.js"></script>
<script src="/js/test2.js"></script>
<script src="/js/test3.js"></script>
每个脚本输出一个简单逻辑:
// test1.js
console.log('test1');
如果资源请求没有问题,通常脚本的执行顺序都符合预期,能看控制台看到对应的输出:
test1
test2
test3
为了模拟复杂的网络环境,假定每个请求都有所延迟(最先请求最晚响应),他们的执行执行顺序是否受影响?
function setTime(path) {
if (path == '/js/test1.js') {
return 3 * 000;
} else if (path == '/js/test2.js') {
return 2 * 1000;
} else {
return 1 * 1000;
}
}
app.use(async (ctx, next) => {
// await lazyLoadScript(ctx.path);
await lazyLoadScript(ctx.path, setTime(ctx.path));
await static(path.join(__dirname, 'public'))(ctx, next);
});
和之前的结果一致。多个脚本异步加载,虽然脚本间响应时间不同,但最终执行顺序和请求顺序一致。
//3 秒后
test1
test2
test3
同时也应证了上面提到的,多个脚本之间不互相阻塞。
defer 和 async
这两个要放在一起讨论,因为都有延迟作用。
defer
最开始提到了 script 的放置位置(header 和 body),defer 属性就可以解决这样的问题。
- defer属性规定脚本立刻下载,但页面加载完成后,才会执行脚本。
- defer属性只适用于外部脚本文件。
<head>
<script defer src="/js/test1.js"></script>
<script defer src="/js/test2.js"></script>
<script defer src="/js/test3.js"></script>
</head>
标记 defer 的脚本标签,即使写在 header 位置,也不会阻塞页面的加载。但先于 document 加载完之前。
同时,这些脚本执行顺序依旧和他们书写的一致,不受延迟时间不同的影响。
async
- async规定脚本立即下载,一旦可用就异步执行。
- async先加载完的脚本,先执行。
async属性和defer属性的不同之处在于何时执行这个脚本。标注有async属性的Script会在下载完成后即可执行,不需要等待window的load事件。这意味着标记有async属性的脚本并不一定会按在页面中嵌入的顺序执行。而标记有defer属性的脚本却一定会按它们在页面上的顺序依次执行。执行会在解析完全完成后开始,但会在document的DOMContentLoaded事件之前。
我们前面已经知道了:脚本之间不受延迟时间的影响,执行顺序和他们请求顺序一致。
如果我们在 test1.js 定义了一个全局变量,即使此脚本会延迟响应很久,但在之后 test3.js 运行时依旧能取到值。
// 延迟 3 秒
console.log('test1');
var globalNum = 1;
// 延迟 1 秒
console.log('test3');
console.log(globalNum);
不过,放在 async 标记的脚本中,执行顺序就不同了:
能看到,最先响应回来的脚本先执行,此时 test1.js 定义的全局变量还没有声明,就会报错。
总结
defer 和 async 主要功能类似,都是为了不阻塞页面内容的渲染。
但在使用 async 属性时,需要特别注意。因为他会脱离脚本之间约定好的顺序,建议在和业务代码不相干的脚本中使用,避免发生脚本之间互相依赖的问题。
另外,建议异步脚本不要在加载期间修改DOM。
document.ready 和 window.onload
document.ready 是 jQuery 里实现的方法,内部其实是对 document 的 DOMContentLoaded 事件做监听。这里就以 document.ready 来示意。
下面,测试他们之间的执行顺序(script 至于 body 底部):
<body>
<script>
window.onload = function () {
console.log('window ready');
};
document.addEventListener('DOMContentLoaded', function () {
console.log('document ready');
});
</script>
<script src="/js/test1.js"></script>
<script src="/js/test2.js"></script>
<script src="/js/test3.js"></script>
</body>
脚本加载和 document.ready 与 window.onload 的顺序如下:
先加载脚本,脚本全部执行后,触发 document 的 DOMContentLoaded 事件,最后执行 window.onload。
那 document.ready 和 window.onload 有什么区别?
首先顺序上,window.onload 晚于 document.ready;另外,如果页面有异步资源(图片),window.onload 会等待图片资源响应完后再触发。
能看到 image 图片加载完后,window.onload 才被调用。
使用 async 时的不同
使用 defer 不会对他们之间的执行顺序造成影响,脚本执行先于 document.ready 执行。
而 async 却会对原先的执行过程有 大改变:
<script>
window.onload = function () {
console.log('window ready');
};
document.addEventListener('DOMContentLoaded', function () {
console.log('document ready');
});
</script>
<script async src="/js/test1.js"></script>
<script async src="/js/test2.js"></script>
<script async src="/js/test3.js"></script>
document.ready 不再等待脚本的加载完成,页面渲染完后则会被触发。async 标识的脚本延迟加载,加载完后立马执行。
动态加载脚本
直接看代码(摘自《高性能 Javascript》):
<script>
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = '/js/test1.js';
document.getElementsByTagName('head')[0].appendChild(newScript);
// 脚本加载完毕
if (newScript.readyState) {
// IE
newScript.onreadystatechange = function () {
if (newScript.readyState == 'loaded' || newScript.readyState == 'complete') {
console.log('loaded', newScript.src);
}
};
} else {
newScript.onload = function () {
console.log('loaded', newScript.src);
};
}
</script>
当页面加载时,只是解析 script 标签内的代码。当 document 全部准备完毕后,将发送请求加载资源。
这样将不影响页面内容的渲染,同时上面代码将动态加载的脚本添加到 head 标签中,能够不受 body 内出现错误的影响。
事实上,应该发现百度统计代码,或者其他平台的一些脚本都是通过这种形式动态将脚本注入到我们页面中的。
作者:Eminoda
链接:https://juejin.cn/post/6844904166528139277
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。