又说 动态加载 script. ie 下 script Element 的 readyState状态

事实 上 说这个话题 有点老生常谈..但是因为涉及到更多 并行加载脚本的 思考 所以 还是觉得可以写一下. 

 

为了 脚本资源的高并行加载 提高页面加载速度.. 我们可能需要动态加载 script...     其中总是无法避免的 一个方法  是 使用  head.appendChild(script) ;  因为这种方式 可以直接跨域.

但是有时候  动态加载脚本可能是要保证 他们的执行时序.   最理想的状态就是  所有的脚本 都可以 在 当前  http连接数 允许的前提下.最大化并行加载量的 同时 .. 可以选择 按时序执行或按加载完成顺序执行 即先到先执行.

 

假设 我们有 3个 js   分别为  a.js  b.js c.js  传统的方式  是这样的 :

 

<script src="a.js"></script>

<script src="b.js"></script>

<script src="c.js"></script>

 

这样的加载 大概 只有 ff3.0+ 和opera 的比较新的版本 可以保证 他们的并行加载  而 其他浏览器 则只会为他们开启 一个http连接 . 一个一个的加载.

让他们在其他浏览器并行加载的方式 有 几种  比如 借助iframe  比如document.write 比如 上面说到的 head.appendChild(script)  比如 xhr 请求 然后eval . 比如 xhr 注入(即 xhr 和head.appendChild(script) 相结合) .   其中 document.write 写入脚本块.的方式有些问题 其中比较头疼的 是  有些浏览器 可能会 警告 说是 危险代码 .以及一些其他问题 . 详细问题 可以去 yslow 作者 blog 去看看.    而另外的 方式 显然 都不能很方便的解决 跨域问题.   所以我们 来探讨下  本文的重点  head.appendChild(script) 方式.

 

这种方式在非ie下 我们可以 很容易的 捕捉  script.onload 以及 script.onerror  但是 ie 我们只能借助 script.onreadystatechange 来判断其加载执行状态.  而 onerror 则必须 和 被请求的.js文件 做某些 约定 才可以更好的判断. 该细节不在本文讨论范围...  

 

在话题继续下去前   我们应该先明确 下  .

 在动态添加script块的情况下 为什么需要 script.onload .     比如 上面的a.js b.js c.js  这三个脚本  假如他们的执行时序 是有依赖性的 话  那么我们就必须 保证a.js b.js c.js按次序执行...  但是 一单他们并行加载  的话  只有 firefox 和opera 才可以保证 他们按照请求发起的 顺序 来执行这些脚本. 而其他浏览器 则会是 无论谁先向服务器发起请求.  都只会是一个结果 即 哪个先加载完毕  先执行哪个.  为了 让我们的Loader API 更健壮... 不得不 在其他浏览器下 牺牲 并行加载 这个好处, 而 改用 加载好一个 执行完毕后 再加载下一个的方式来处理... 当然 这里我们有 另外的解决 方案 留到后面说.    现在 我们把需求简化一下   我们仅仅想要 script 加载并且 执行完毕后 回调我们指定的方法.

对于非ie  非常简单  如下面的代码 :

var script = document.createElement('script');

script.onload=function () {alert('callBack');};

script.src="a.js" ;

document.getElementsByTagName('head')[0].appendChild(script);

 

就是如此简单 对吧 . 但是遗憾的是 ie并不支持 script.onload事件  这时候我们 只好借助 

script.onreadystatechange=function(){ script.readyState=='某个值'}  

这种方式来判断 脚本是否 加载 并执行完毕 

此时 readyState 的值  可能为 以下几个 :

  • “uninitialized” – 原始状态 
  • “loading” – 下载数据中..
  • “loaded” – 下载完成
  • “interactive” – 还未执行完毕.
  • “complete” – 脚本执行完毕.

现在 问题来了 . ie6 和 ie7 ie8 有些区别  大致分为以下几种情况  (以下状态 本人测试后 又请几位朋友帮忙测试  应该可以信任.)

script.src="a.js" ;

document.getElementsByTagName('head')[0].appendChild(script);

先写src 后append 的情况下  complete 和 loaded 只有一个 会出现. 他都标志着 脚本加载 并执行完毕   但出现哪个 有时候却不能确定 和 加载脚本时间 以及 脚本执行时间 都有关系.

 

document.getElementsByTagName('head')[0].appendChild(script);

script.src="a.js" ;

先append 后给src 则  比较其开怪 ie 就可能同时 出现 complete 和loaded  而 其中 ie7 和ie8 总能保证 loaded 为 最后一个状态  即 此时 ie7 8 我们可以信任 loaded 状态时 脚本 已经加载 执行完毕 .  但是 ie6 就比较郁闷 它会 因为 脚本加载时间 和 脚本加载后执行时间不同   导致 loaded 和 complete 的出现先后次序 的不同...这时候我们无法得知  哪个状态 才是 脚本执行完毕的状态...    为了判断 这种状态下 脚本是否执行完毕  我们需要借助 额外的 开关变量来 信任 最后出现的 那个 状态 才是 脚本执行完毕的 状态.  

 

经过大量测试后  得出一个 结论 即  如果 你想少惹麻烦的话  请 先给script 设置 src属性  然后 再 appedChild 他 到 DOM树中...  这样的话 我们 就只需要  这种代码 即可以 判断  脚本执行结束 了.

 

if (/loaded|complete/.test(script.readyState)) //ie6 ie7 ie8 通用.  好了 解决了readyState问题后 我们 去看看另外的问题

 

 

 大神  Nicholas C. Zakas 给出的方案如下 :

 

 

	if (script.readyState) { //IE
		script.onreadystatechange = function(){
       		a.push(script.readyState);
	 		if (script.readyState == "loaded" || script.readyState == "complete") {
	    		script.onreadystatechange = null;
	    		callback && callback();
	   		}
		};

	}
	else { //Others
		script.onload = function(){
			callback();
		};
	}

 

 

 

很明显 大神忽略了 两问题.

1. script.onreadystatechange=function(){} 这种方式 会造成某些版本的ie6无法挽回的 cross page leak 内存泄露 (暂时我只能确定可以确定sp3补丁的ie6没这个问题 ) 所以即使 他具备 script.onreadystatechange = null; 对于ie6没有意义

2. opera 也支持 readyState这个事实 所以他这段脚本 在opera比较新的浏览器下 就会出问题...    另外 一但 将来的某个ie版本支持了 script.onload 我们可能 就无法享受到这个好处 了

 

对于1 

建议使用attachEvent 请记得 对于ie6 任何非 attachEvent方式注册的事件 (除硬编码写到html中的) 都会引起ie6 无法挽回的 跨页内存泄露. 至于多少 就看回调函数 所在闭包  中的数据量了. 作用域链 越深 受影响的东西 就越多...危害也就越大. 

 

对于2 

我觉得 还是应该 优先判断是否支持 onload 才是正确的思路  比如这样:

 

 

function isImplementedOnload(script){
  script = script || document.createElement('script') ;
  if('onload' in script) return true ; 
	script.setAttribute('onload','');
	return typeof script.onload == 'function' ; // ff true ie false .
}

 

很显然 一来二去的  随着我们代码量的增加 现在 script 块 动态加载脚本 显得越来越靠谱了....

 

那么我们说说 应用中的一些问题

 

理想状态下  所有脚本 都应该 最大化的 利用 当前可用的http连接数  即有几个我就用机个 去并行加载脚本 而不是一个一个的在那里阻塞... 但是很显然 想做到面面俱到 是不容易的事情.

那么 应该有以下一个 流程来处理他们

 

1.  优先考虑 xhr eval 或 xhr 注入  这两种方式 可以在保证并行下载资源的同时  很方便的 控制 执行的时序.

2. 一但无法解决跨域问题  则 使用 script 块 动态加载的方式  但应知道 此种方式 非ff opera浏览器 一旦要求执行时序 则我们 无法达成  并行加载 这一最初的 目的.

3. 群里的朋友  瓶子 给出的方案 是  可以借用 postMessage    ie6 7使用 window.opener 漏洞  去实现域通信 然后借助一个 被引入的iframe页面 去请求脚本 然后 把 脚本内容 传给父页 

 

方案3  瓶子给出的demo  : http://www.webairness.com/apps/libs/local2.html

 

 

 

好吧回到最初的话题  请记得 对于动态加载 scirpt块  仍然可能 阻塞window.onload的情况 当然(硬编码的 轻轻是一定会阻塞的啦) 我们可以 把请求代码 写到 setTimeout  1ms 中  这样 就可以更早的 window.onload   越早onload的好处 就是 当我们  在 window.onunload 释放一些 事件侦听回调函数 以避免 内存泄露时 变的尤其重要  .  因为 有些浏览器  onload不发生   就永远不会发生 onunload  这是个很无奈的问题.... 这也是为什么推荐少使用iframe去 解决 一些跨域 问题的初衷...因为firame 会阻塞 主页面的 onload ....

 

暂时就到这里吧.... 如果有遗漏 或错误 欢迎指正.....

 

if (script.readyState){  //IE
        script.onreadystatechange = function(){
            if (script.readyState == "loaded" ||
                    script.readyState == "complete"){
                script.onreadystatechange = null;
                callback();
            }
        };
    } else {  //Others
        script.onload = function(){
            callback();
        };
    }if (script.readyState) { //IE
script.attachEvent('onreadystatechange', function(){
a.push(script.readyState);
if (script.readyState == "loaded" ||
script.readyState == "complete") {
script.onreadystatechange = null;
callback && callback();
}
});
}
else { //Others
script.onload = function(){
callback();
};
}
posted @ 2010-06-20 13:59  Franky  阅读(...)  评论(...编辑  收藏