為了 指令碼資源的高並行載入 提高頁面載入速度.. 我們可能需要動態載入 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 給出的方案如下 :
view sourceprint?
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(); |
12 |
script.onload = function(){ |
很明顯 大神忽略了 兩問題.
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 才是正確的思路 比如這樣:
view sourceprint?
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 . |
很顯然 一來二去的 隨著我們代碼量的增加 現在 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 ....