为了 脚本资源的高并行加载 提高页面加载速度.. 我们可能需要动态加载 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 给出的方案如下 :
01 |
if (script.readyState) { //IE |
02 |
script.onreadystatechange = function (){ |
03 |
a.push(script.readyState); |
04 |
if (script.readyState == "loaded" || script.readyState == "complete" ) { |
05 |
script.onreadystatechange = null ; |
06 |
callback && callback(); |
07 |
} |
08 |
}; |
09 |
10 |
} |
11 |
else { //Others |
12 |
script.onload = function (){ |
13 |
callback(); |
14 |
}; |
15 |
} |
很明显 大神忽略了 两问题.
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 才是正确的思路 比如这样:
1 |
function isImplementedOnload(script){ |
2 |
script = script || document.createElement( 'script' ) ; |
3 |
if ( 'onload' in script) return true ; |
4 |
script.setAttribute( 'onload' , '' ); |
5 |
return typeof script.onload == 'function' ; // ff true ie false . |
6 |
} |
很显然 一来二去的 随着我们代码量的增加 现在 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 ....