加载和执行
脚本阻塞
JavaScript语言是单线程的,意味着同一时间只能做一件事。
其主要原因是因为JavaScript作为浏览器脚本语言,它的主要功能是与使用者互动以及操作DOM元素。如果JavaScript采用多线程就会出现一个很严重的问题,假设有两个线程同时运行,一个线程在删除DOM元素,而另一个线程在添加DOM元素,此时浏览器该以哪一个线程为主?
这就意味着每当有< script>标签出现时,就会让页面等待脚本文件的加载和执行,无论< script>是内嵌或外链形式,其中的原因是因为浏览器无法预知脚本中是否含有修改DOM树的操作。
脚本位置
推荐将< script>标签尽可能的放置在< body>标签的尾部。
虽然目前浏览器基本执行并行下载JavaScript文件,但仍然会阻塞其他资源文件的下载,而这也就意味着页面仍然会等待脚本文件的加载和执行后才能继续执行。
当浏览器执行到< body>标签的尾部时页面基本已经渲染完全,但当脚本位置出现在文档头部时会导致页面在加载与执行脚本代码期间页面内容为空白,脚本位置出现在文档中部会导致页面渲染会卡住,这两种情况无论哪一种出现无疑都会造成极差的用户体验。
无阻塞脚本
无阻塞脚本的秘诀在window对象的load事件触发后在开始下载脚本,也就是说在页面加载完成后才加载JavaScript代码。
有以下几种方式可以实现这个功能。
1.defer属性
在html4中加入了一个扩展属性:defer。
当一个带有defer属性的JavaScript文件下载时,它可以并行下载此页面的其他资源文件,不会阻塞浏览器的其他进程。
它的使用前提是,此脚本代码中不含有更改DOM元素的代码。
<script type="text/javascript" src="xxx.js" defer="defer"></script>
带有defer属性的< script>标签可以放置在文档的任意位置,页面解析到< script>标签时开始下载,但并不会执行,直到DOM加载完成(onload事件被触发)。
以下代码在不支持defer属性的情况下执行顺序为defer、script、load, 在支持defer属性的情况下执行顺序为script、defer、load。
注意:带有defer属性的< script>元素不是跟在第二个后面执行的,而是在onload事件执行之前被调用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./加载和执行.js" defer></script>
<!--alter("defer");defer属性仅适用于外部脚本 -->
<script>
alert("script");
</script>
<script>
window.onload = () => alert("load");
/*相当于
window.onload = function(){
alert("load");
}
*/
</script>
</body>
</html>
2.async属性
在html5中加入了一个新扩展属性:async。
async属性同样可以异步加载JavaScript脚本,但与defer区别在于它们的执行时间有差异,defer在页面完成后才执行,async则是在下载完成后立即执行,而且async的执行并没有严格按照页面中script的顺序而是谁先加载完就先执行谁。
<script type="text/javascript" src="xxx.js" async="async"></script>
与defer相同,async同样仅适用于外链脚本。
3. 动态脚本元素
由于文档对象模型(DOM)存在,< script>标签这个与其他元素并无差异的标签也可以通过DOM进行引用,都可以在文档中移动、删除、或者是被创建。
在除IE以外的浏览器,< script>元素接收完成后会触发一个load事件。因此我们可以监听这个事件来获取脚本完成时的状态。
var script = document.createElement("script");
script.type = "text/javascript";
script.onload = () => alert("script loaded");
script.src = "./加载和执行.js"; //alert("加载与执行");
document.head.appendChild(script);
/*
由于load事件是在加载完成时才会触发,所以以上代码的执行顺序为:
加载与执行
script loaded
*/
在IE浏览器中,会触发一个readystatechange事件。< script>元素提供了一个readystate属性,该属性有五个取值供我们判断加载过程中的不同阶段。
属性名 | 解释 |
---|---|
uninitialized | 初始状态 |
loading | 开始下载 |
loaded | 下载完成 |
interactive | 数据下载完成但尚不可用 |
complete | 所有数据准备就绪 |
在实际应用中,最常用的两个状态时"loaded"和"complete"。在IE中使用readystatechange事件最靠谱的方式是同时检查这两种状态,因为有时会到达"loaded"状态而不到达"complete"状态,又是甚至不经过"loaded"状态直接到达"complete"状态,因此我们最好在其中一个状态触发时就删除掉事件处理器以确保事件不会被触发两次。
var script = document.createElement("script");
script.type = "text/javascript";
//IE
script.onreaystatechange = () => {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreaystatechange = null;
alert("script loaded");
}
}
script.src = "./加载和执行.js";
document.head.appendChild(script);
为了保证浏览器兼容性,以及方便我们调用所以把它们封装成一个方法。
//调用格式
loadScript("./加载和执行.js", function() {
alert("file is loaded");
});
//封装方法
function loadScript(url, callback) {
var script = document.createElement("script");
script.type = "text/javascript";
if (script.readyState) { //IE
script.onreadystatechange = () => {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
callback();
}
}
} else { //其他浏览器
script.onload = () => callback();
}
script.src = url;
document.head.appendChild(script);
}
动态脚本加载凭借它的跨浏览器兼容性和易用的优势,成为最通用的无阻塞加载解决方法。
4. XMLHttpRequest脚本注入
此技术会先创建一个XHR对象,然后用它下载JavaScript文件,最后通过创建动态< script>元素将代码注入页面。
var xhr = new XMLHttpRequest();
xhr.open("get", "加载和执行.js", "true");
xhr.readyStatechange = function() {
if (readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
}
xhr.send(null);
这段代码使用了AJAX同源的方式获取了文件,当收到有效响应时就会创建一个< script>元素。这个方式的优点就是下载JavaScript代码后不会立即执行,你可以把你的脚本的执行推迟到你准备好的时候,而且不需要考虑浏览器的兼容性。
但缺点也很明显,它是同源的,不能跨域,所以基本不会使用这个技术。