文章比较长,真正需要的就耐心看吧。

比如我们要加载a.js,一般会这么写:

var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'a.js';
head.appendChild(script);

 说一个知识点,后面会用到:

Opera这货是个彻彻底底的两面派,比如它支持 IE 的attachEvent,也支持标准的addEventListener; 它支持IE的currentStyle,也支持标准的window.getComputedStyle;不一而足。 

所以有时候专门针对IE的fix,需要排除Opera,因为它既然实现了更好的方式,我们就要用更好的,我们的目的是落后的IE,仅此而已!

Opera检测技巧:var isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]';

如果我们需要调用 a.js 里的fn()方法,因为这个过程是异步的,所以要等到js文件加载完成时才能调到,怎么判断是否完成加载呢?

var isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
    head = document.getElementsByTagName('head')[0],
    script = document.createElement('script');
script.type = 'text/javascript';
if (script.attachEvent && !isOpera) {
    script.attachEvent('readystatechange', onScriptLoad);
} else {
    script.addEventListener('load', onScriptLoad);
}
script.src = 'a.js';
head.appendChild(script);

悬念继续留给 onScriptLoad 方法,这里再插一个知识点:readyState,它包括以下值:

  0: "uninitialized" – 原始状态

  1: "loading" – 正在加载

  2: "loaded" – 加载完成

  3: "interactive" – 还未执行完毕

  4: "complete" – 脚本执行完毕

为什么我写了 ":" 呢,因为在 xhr 中,请求完成时的readyState为数字形式,即 ":" 左侧的部分,而以节点加载时,readyState为字符串形式,即 ":" 右侧的部分。其中涉及的兼容性问题,请参看PPK

为什么要说这个呢,当然是为了IE 这厮。其他浏览器在脚本加载完成时,会发出 onload 事件,所以不存在问题,但是IE不知onload为何物,所以它独创一派。

经我测试Chrome14, Firefox8, Opera11, Safari5, onload 事件触发的时机是在脚本执行完之后,比如请求 a.js,这个文件的最后一行写上 alert('xxx'); 然后 script.onload = function() { alert('onload'); },打印顺序一致是 xxx -> onload。

我又试了script.addEventListener('load', function() { alert('onload'); },结果相同。

IE支持 onreadystatechange 事件,Opera 则两个都支持,同样的还是要过滤掉Opera。肯定有人会问,那IE9呢?好吧,我承认我不清楚,我只是听说IE9的addEventListener方法和别的标准浏览器表现不太一致,所以在这把IE9一并归入传统IE浏览器的范畴了。

怎么判断脚本是否加载完成呢?一般的做法是判断script.readyState,IE就是个变态,连这个值都不是固定的,所以需要这么做:

script.readyState === 'loaded' || script.readyState === 'complete'

关于这里的加载判断,我参考了好几个框架的设计,除了RequireJS 多一句 event.type === 'load',大多都是用上面这段,所以咱也用这句,要死大家一起死吧。

还有一点,因为我们统一放在 onScriptLoad 里处理,而在标准浏览器中 script.readyState 为 undefined; 为了防止内存泄漏,最好在加载完成后把 script 节点移除,参看代码:

function onScriptLoad(e) {
	e = e || window.event;
	var script = e.target || e.srcElement;
	if (/loaded|complete|undefined/.test(script.readyState)) {
		if (script.detachEvent && !isOpera) {
			script.detachEvent('onreadystatechange', onScriptLoad);
		} else {
			script.removeEventListener('load', onScriptLoad);
		}
		var head;
		if (head = script.parentNode) {
			try {
				if (script.clearAttribute) {
					script.clearAttribute();
				} else {
					for (var prop in script) {
						delete script[prop];
					}
				}
			} catch (e) { }
			head.removeChild(script);
		}
	}
}

再来看一个问题,现在有个需求,比如脚本 a 依赖 脚本b,a 肯定要等到 b 加载并执行完之后才能开始执行,这怎么办呢?

1. 串行加载,即一个加载完再加载下一个(较慢)

2. 并行加载

第一种方法没什么可说的,这里说第二种。

先介绍一个属性:async

当 script 的 async 属性为 true 时,脚本的执行序为异步的。即不按照加入 DOM 的顺序执行;如果是 false 则按加入的顺序执行。

如果 script 标签被直接编码到 HTML 中,黙认的 async 属性为 false;如果 script 是由 document.createElement('script') 创建的,那么 async 属性为 true。

检测方法: var script = document.createElement('script'); script.async === true;

检测结果: IE6-9, Opera11, Safari5 不支持

再介绍一个属性:defer

defer 属性规定是否延迟执行脚本,直到页面加载为止,默认值为false,具体情况可参考我最后给出的链接

触发方式:script.defer = 'defer'

检测方式:var script = document.createElement('script'); script.defer === false;

检测结果:所有浏览器都支持

相同点

不同点

带有 async 或 defer 的 script 都会立刻下载,不阻塞页面解析,而且都提供一个可选的 onload 事件处理,在 script 下载完成后调用,用于做一些和此 script 相关的初始化工作。

script 执行的时机不同。

带有 async 的 script,一旦下载完成就开始执行(当然是在window的onload之前)。这意味着这些 script 可能不会按它们出现在页面中的顺序来执行,如果你的脚本互相依赖并和执行顺序相关,就有很大的可能出问题。

而对于带有 defer 的 script,它们会确保按在页面中出现的顺序来执行,它们执行的时机是在页面解析完后,但在 DOMContentLoaded 事件之前。

接着讲刚才的问题,我们关心的不是谁先加载,而是谁先执行,是执行顺序的问题,所以如果浏览器支持 async 属性,记得设置为false,然后按你需要的顺序进行appendChild就行了,但是这种方式明显是不兼容的...

还好,我们还有defer,浏览器都支持。

最后用 Script 方式 和 XHR 方式做个比较:

优点

缺点

1. 具有跨域能力

2. 即使 ActiveX 被关了也可以在 IE 中运行

3. 可以在不支持 xhr 的老旧浏览器上运行

1. 返回的数据必须格式化为js代码,而 xhr 返回的数据可以是任何格式,XML, JSON, 纯文本等等

2. 只支持GET请求,不支持POST

3. 请求是异步还是同步完全取决于浏览器,而 xhr 可以由你控制

4. 当从一个不受信任的来源获取JSON数据时,你没办法在代码执行前检查这些数据,而 xhr 可以用一些工具分析数据,比如json2.js

还有一个不同就是,动态创建script节点所加载的文件,如果在当前上下文调用eval()来处理,那么该文件中定义的变量和函数都是全局的。如果你希望加载的数据只是局部可用,那就用 xhr 吧。

这两种方式各有各的好处,总的来说,如果是需要加载一段代码,最好使用 动态创建script节点 的方式,如果是请求数据,最好使用 xhr。