这两天笔者看到这方面的资料,跟大家讨论分析一下在加载JS文件的时候,我们常常会发现几个问题:
- 同步脚本和异步脚本带来的文件加载、文件依赖及执行顺序问题
- 同步脚本和异步脚本带来的性能优化问题
以下都以引用如下js文件为例:
<script src="test.js"></script>
将script文件放入header标签
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>加载js文件的问题<title>
<script src="test.js"></script>
</head>
<body>
<p>内容内容内容内容内容</p>
<img src="pic.png" />
</body>
</html>
其中,test.js中的内容——
alert('我是head里面的脚本代码,执行这里的js之后,才开始进行body的内容渲染!');
如上的代码会阻塞所有页面渲染工作,使得用户在脚本加载完毕并执行完毕之前一直处于“白屏”状态。但是要注意,此时整个页面已经加载完毕,如果body中包含某些src属性的标签(如上面的img标签),此时浏览器已经开始加载相关内容了。总之要注意——js引擎和渲染引擎的工作时机是互斥的(一些书上叫它为UI线程)。
因此,我们要先加载CSS等文件,再加载脚本。
script脚本延迟执行
我们现在一般会把脚本放在body结尾标签的上方,这样一方面用户可以更快地看到页面,另一方面脚本可以直接操作已经加载完成的dom元素。如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>加载js文件的问题<title>
</head>
<body>
<p>内容内容内容内容内容</p>
<img src="pic.png" />
<script src="test.js"></script>
</body>
</html>
这样确实极大地加快了页面渲染时间,然而,这有可能会让用户在script加载之前与页面上的按钮等进行交互。这是因为浏览器在加载完整个文档之前无法加载这些放在
结尾上面的script脚本的,也就是说,当有加载缓慢的大型文档来说会是一个很大的瓶颈。因此,理想情况是,脚本加载与文档加载同时进行,而且还不影响DOM渲染。这样,一旦文档就绪就可以运行脚本。
defer属性
上面的问题,用defer可以解决。代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>加载js文件的问题<title>
<script src="deferredScript.js" defer></script>
</head>
<body>
<p>内容内容内容内容内容</p>
<img src="pic.png" />
</body>
</html>
添加defer属性相当于告诉浏览器:请马上开始加载这个脚本吧,但是,请等到文档就绪之后再运行它。即,立即下载但延迟执行。若碰到多个defer脚本,第一个defer先于第二个defer执行。
然而,defer有兼容问题,这意味着,如果想确保自己的延迟脚本能在文档加载后运行,就必须将所有延迟脚本的代码都封装在诸如jQuery之$(document).ready之类的结构中。但使用defer的时候要注意两个问题:
1、不要在defer型的脚本程序段中调用document.write命令,因为document.write将产生直接输出效果。
2、而且,不要在defer型脚本程序段中包括任何立即执行脚本要使用的全局变量或者函数。
因此,有没有更好的方法?我不想等到defer脚本一个接着一个运行(defer让我们想到一种静静等待文档加载的有序排队场景),更不想等到文档就绪之后才运行这些脚本,我想要尽快加载并且尽快运行这些脚本。这里也就想到了HTML5的async属性。
async属性
我们加载两个完全不相干的第三方脚本,页面没有它们也运行得很好,而且也不在乎它们谁先运行谁后运行。因此,对这些第三方脚本使用async属性,相当于一分钱没花就提升了它们的运行速度。
async属性是HTML5新增的。作用和defer类似,即允许在下载脚本的同时进行DOM的渲染。但是它将在下载后尽快执行(即JS引擎空闲了立马执行),不能保证脚本会按顺序执行。它们将在onload 事件之前完成。
Firefox 3.6、Opera 10.5、IE 9 和 最新的Chrome 和 Safari 都支持 async 属性。可以同时使用 async 和 defer,这样IE 4之后的所有 IE 都支持异步加载,但是要注意,async会覆盖掉defer。
<!DOCTYPE html>
<html>
<head lang="en">
<script src="headScript.js"></script> <script src="deferredScript.js" defer></script>
</head>
<body>
<script src="asyncScript1.js" async defer></script>
<script src="asyncScript2.js" async defer></script>
</body>
</html>
如上代码的执行顺序是:
1. 各个脚本加载;
2. 执行headScript.js;
3. DOM渲染的同时会在后台加载defferedScript.js
4. DOM渲染结束时将运行defferedScript.js和那两个异步脚本(注意对于支持async属性的浏览器而言,这两个脚本将做无序运行)
然而,async也有兼容问题,而且应用也不是很广泛,因此我们更多地使用脚本加载其他脚本。
可编程的脚本加载
在浏览器API层面,有两种合理的方法来抓取并运行服务器脚本——
- 生成ajax请求并用eval函数处理响应
- 向DOM插入script标签
后一种方式更好,因为浏览器会替我们操心生成HTTP请求这样的事。再者,eval也有一些实际问题:泄露作用域,调试搞得一团糟,而且还可能降低性能。这里有个封装好的加载js的文件:
var loadJS = function(url,callback){
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.src = url;
script.type = "text/javascript";
head.appendChild( script);
// script 标签,IE下有onreadystatechange事件, w3c标准有onload事件
// IE9+也支持 W3C标准的onload
var ua = navigator.userAgent,
ua_version;
// IE6/7/8
if (/MSIE ([^;]+)/.test(ua)) {
ua_version = parseFloat(RegExp["$1"], 10);
if (ua_version <= 8) {
script.onreadystatechange = function(){
if (this.readyState == "loaded" ){
callback();
}
}
} else {
script.onload = function(){
callback();
};
}
} else {
script.onload = function(){
callback();
};
}
};
总结
下面我再总结一下关于异步加载的陈述:
一. 传统方式(同步执行脚本,即脚本并行加载,只不过有出发请求先后的区别)
- 嵌入到head标签中——要注意,这样做并不会影响文档内容中其他静态资源文件的并行加载,它影响的是,文档内容的渲染,即此时的DOM渲染就会被阻塞,呈现白屏。
- 嵌入到body标签底部——为了免去白屏现象,我们优先进行DOM的渲染,再去执行脚本,但问题又来了。先说第一个问题——如果DOM文档内容比较大,交互事件绑定便有了延迟,体验便差了些。当然,我们需要根据需求而定,让重要的脚本优先执行。再说第二个问题——由于脚本文件至于body底部,导致对于这些脚本的加载相对于至于head中的脚本而言,它们的加载便有了延迟。所以,至于body底部,也并非是优化的终点。
- 添加defer属性——我们希望脚本尽早的进行并行加载,我们把这批脚本依旧放入head中。脚本的加载应该与文档的加载同时进行,并且不影响DOM的渲染。这样,一旦文档就绪就可以运行脚本。所以便有了defer这样属性。但是要注意它的兼容性,对于不支持defer属性的浏览器,我们需要将代码封装在诸如jQuery之$(document).ready中。需要注意一点,所有的defer属性的脚本,是按照其出场顺序依次执行,因此,它同样严格同步。
二. “并行执行脚本”(即谁先加载完了,只要此时js引擎空闲,立马执行)
- 添加async这个属性——确实能够完成上面我们所说的优化点,但是它有很高的局限性,即仅仅是针对非依赖性脚本加载,最恰当的例子便是引入多个第三方脚本了。还有就是与deffer属性的合用,实在是让人大费脑筋。当然,它也存在兼容性问题。以上三个问题便导致其应用并不广泛。当使用async的时候,一定要严格注意依赖性问题。
- 脚本加载脚本——很显然,我们使用之来达到“并行执行脚本”的目的。同时,我们也方便去控制脚本依赖的问题,我们便使用了如requirejs中对于js异步加载的智能化加载管理。后期,笔者会对requirejs等模块化加载进行详细说明。