Javascript加载和执行
一、阻塞脚本
1、脚本位置
大多数浏览器使用单进程处理UI更新和Javascript执行等多个任务,而同一时间只能有一个任务被执行。不论Javascript代码是内联还是通过外部文件引入,当Javascript脚本被执行时,浏览器不能处理其他事情,等待脚本被执行完成后,浏览器才能继续解析页面。
因此,通常是将所有的<script>
标签放在尽可能接近</body>
标签的位置,以减少对整个页面下载解析的影响。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<p>content</p>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
2、打包脚本
由于每个<script>
标签下载时会阻塞页面解析过程,因此限制<script>
标签的总数也可以改善性能。如果脚本是通过外部文件引入的,每加载一个Js文件时发送的HTTP请求都会产生额外的性能负担,可以将多个文件合并成一个文件来减少性能损失。
http://www.example.com/combo?build/action/action-min.js&build/event/event-min.js
二、非阻塞脚本
非阻塞脚本是在等待页面加载完成后,再执行JavaScript脚本,这种脚本的加载和执行不会阻塞页面的加载解析。有以下几种方式可以实现:
1、延迟(Deferred)脚本
在<script>
标签上使用defer
属性
<script type="text/javascript" src="script.js" defer></script>
defer
属性表示此脚本可以稍后执行,此属性被大多数高版本浏览器支持。带有defer
属性的<script>
标签可以放在文档的任意位置,对应的脚本文件将在<script>
标签被解析时开始下载,但代码不会被执行,直到DOM加载完成(onload事件句柄被调用之前)后执行。而且这种脚本文件下载时不会阻塞浏览器的其他处理过程,因此可以与页面的其他资源并行下载。
defer
属性不能用在没有src属性的<script>
标签上,如果在内联脚本上添加defer
属性则不会有任何效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript</title>
<script src="./script.js" defer></script>
</head>
<body>
<script defer>
//defer属性在此处不会生效
console.log('inline defer');
</script>
<script>
console.log('script');
window.onload = function(){
console.log('load');
};
</script>
</body>
</html>
./script.js
:
console.log('file defer');
结果:
2、动态脚本元素
可以使用DOM API动态创建<script>
元素:
var script = document.createElement("script");
script.src = "dynamic.js";
document.querySelector('head').appendChild(script);
使用此种方式,无论创建的<script>
标签位于何处,脚本的下载和执行都不会阻塞其他页面处理过程;脚本下载完成后会立即执行其中的代码,可以使用onload
(IE:readystatechange
)事件句柄来实现脚本下载执行完成后的操作。
var script = document.createElement("script");
script.src = "dynamic.js";
script.onload = function(){
console.log('dynamic script loaded!');
};
document.querySelector('head').appendChild(script);
//代版本IE
script.onreadystatechange = function(){
var state = script.readyState;
if(state === 'loaded' || state === 'completed'){
script.onreadystatechange = null;
console.log('dynamic script loaded!');
}
};
可以将动态加载脚本的代码抽取为一个函数:
function loadScript(url, callback){
var script = document.createElement("script");
script.src = url;
if(script.readyState){
//兼容低版本IE
script.onreadystatechange = function(){
var state = script.readyState;
if(state === 'loaded' || state === 'completed'){
script.onreadystatechange = null;
callback();
}
};
}else{
script.onload = function(){
callback();
};
}
document.querySelector('head').appendChild(script);
}
下面的例子用来测试脚本的执行顺序:
<body>
<script>
console.log('script');
window.onload = function(){
console.log('window loaded!');
};
</script>
<script>
//此处省略了loadScript函数的定义
loadScript('./dynamic.js', function(){
console.log('dynamic.js loaded!');
});
</script>
</body>
dynamic.js
:
console.log('execute dynamic javascript');
结果:
- 加载多个动态脚本
如果需要按顺序加载多个动态脚本,可以使用以下方式:
loadScript('script1.js', function(){
loadScript('script2.js', function(){
loadScript('script3.js', function(){
console.log('loaded!');
});
});
});
更好的方式是将这些文件按照正确的次序连接成一个文件,一次下载所有代码。
动态脚本加载是非阻塞JavaScript下载中最常用的模式,因为它可以跨浏览器而且简单易用。
3、XHR脚本注入
可以使用XHR对象下载JavaScript文件,然后将代码注入到动态<script>
元素中(相当于动态创建含有内联代码的<script>
元素):
var xhr = new XMLHttpRequest();
xhr.open('get', 'script.js', true);
xhr.onreadystatechange = function(){
var status = null;
var script = null;
if(xhr.readyState === 4){
status = xhr.status;
if(status >= 200 && status < 300 || status === 304){
script = document.createElement('script');
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
};
xhr.send();
-
优点
-
可以下载不立即执行(推迟执行)的JavaScript代码
-
跨浏览器
-
-
缺点
- JavaScript文件必须与HTML页面在同一个域中,不能从CDN中下载,因此,大型网页通常不使用此技术
4、推荐的模式
推荐的向页面加载大量JavaScript的方法如下:
-
引入动态加载JavaScript脚本所需的代码(例如上面的
loadScript
函数) -
加载其他的JavaScript代码
<script type="text/javascript" src="loader.js"></script>
<script type="text/javascript">
loadScript('the-rest.js', function(){
doSomething();
});
</script>
建议将以上代码放置在</body>
标签之前,因为这样可以确保JavaScript代码的运行不会影响页面其他部分的显示,而且JavaScript文件下载完成后,应用所需的DOM已经创建好了,可以避免使用额外的事件处理(例如:window.onload
)来得知页面是否已准备好。
也可以直接将loadScript()
函数嵌入到HTML页面中,这样可以避免一次HTTP请求。
5、非阻塞JavaScript加载库
LABjs
LABjs对加载过程进行更精细的控制,并尝试下载尽可能多的代码:
<script type="text/javascript" src="./LAB.src.js"></script>
<script type="text/javascript">
$LAB.script('base.js')
.script('script.js')
.wait(function(){
console.log('ok!');
});
</script>
其中script()
函数用来下载一个JavaScript
脚本文件,wait()
函数用来指定一个在脚本文件下载并执行完之后被调用的函数。
LABjs
能够管理依赖关系,控制并行下载的动态脚本的执行顺序,它使用wait()
涵数指定哪些文件应该等待其他文件。上面的例子中,base.js
的代码不能保证在script.js
之前执行,因此需要在第一个script()
函数之后使用wait()
函数:
$LAB.script('base.js').wait()
.script('script.js')
.wait(function(){
console.log('ok!');
});
LazyLoad
LazyLoad提供了一个更强大的loadScript()
函数,它可以下载多个JavaScript文件,并保证它们在所有的浏览器上都能够按正确的顺序执行。
<script type="text/javascript" src="lazyload.js"></script>
<script type="text/javascript">
LazyLoad.js(['base.js', 'script.js'], function(){
console.log('ok!');
});
</script>
LazyLoad.css('style.css', function(arg){
console.log(arg.active);//true
console.log(this.foo);//bar
}, {active: true}, {foo: 'bar'});
参考资料
- 《高性能Javascript编程》