最近在作一個爲網頁生成目錄的工具awesome-toc,該工具提供了以jquery插件的形式使用的代碼,也提供了一個基於Bookmarklet(小書籤)的瀏覽器插件。javascript
小書籤須要向網頁中注入多個js文件,也就至關於動態加載js文件。在編寫這部分代碼時候遇到坑了,因而深究了一段時間。html
我在這裏整理了動態加載js文件的若干思路,這對於理解異步編程頗有用處,並且也適用於Nodejs。html5
代碼整理在了https://github.com/someus/how-to-load-dynamic-script。java
若是html中有:jquery
<script type="text/javascript" src="1.js"></script> <script type="text/javascript" src="2.js"></script>
那麼,瀏覽器解析到git
<script type="text/javascript" src="1.js"></script>
會中止渲染頁面,去拉取1.js
(IO操做),等到1.js
的內容獲取到後執行。 1.js執行完畢後,瀏覽器解析到es6
<script type="text/javascript" src="2.js"></script>
進行和1.js
相似的操做。github
不過如今部分瀏覽器支持async屬性和defer屬性,這個能夠參考:web
async vs defer attributes
script的defer和asyncajax
script -MDN指出:async對內聯腳本(inline script)沒有影響,defer的話因瀏覽器以及版本不一樣而影響不一樣。
舉個實際的例子:
<html> <head></head> <body> <div id="container"> <div id="header"></div> <div id="body"> <button id="only-button"> hello world</button> </div> <div id="footer"></div> </div> <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js" type="text/javascript"></script> <script src="./your.js" type="text/javascript"></script> <script src="./my.js" type="text/javascript"></script> </body> </html>
js/your.js:
console.log('your.js: time='+Date.parse(new Date())); function myAlert(msg) { console.log('alert at ' + Date.parse(new Date())); alert(msg); } function myLog(msg) { console.log(msg); }
js/my.js:
myLog('my.js: time='+Date.parse(new Date())); $('#only-button').click(function() { myAlert("hello world"); });
能夠看出jquery
、js/your.js
、js/my.js
三者的關係以下:
js/my.js
依賴於jquery
和js/your.js
。jquery
和js/your.js
之間沒有依賴關係。瀏覽器打開index00.html
,等待js加載完畢,點擊按鈕hello world
將會觸發alert("hello world");
。
firbug控制檯輸出:
下面開始探索如何動態加載js文件。
文件js/loader01.js內容以下:
Loader = (function() { var loadScript = function(url) { var script = document.createElement( 'script' ); script.setAttribute( 'src', url+'?'+'time='+Date.parse(new Date())); // 不用緩存 document.body.appendChild( script ); }; var loadMultiScript = function(url_array) { for (var idx=0; idx < url_array.length; idx++) { loadScript(url_array[idx]); } } return { load: loadMultiScript, }; })(); // end Loader
index01.html內容以下:
<html> <head></head> <body> <div id="container"> <div id="header"></div> <div id="body"> <button id="only-button"> hello world</button> </div> <div id="footer"></div> </div> <script src="./js/loader01.js" type="text/javascript"></script> <script type="text/javascript"> Loader.load([ 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', './js/your.js', './js/my.js' ]); </script> </body> </html>
瀏覽器打開index01.html
,點擊按鈕hello world
,會發現什麼都沒發生。打開firebug,進入控制檯,能夠看到這樣的錯誤:
很明顯,my.js
沒等jquery就先執行了。又因爲存在依賴關係,腳本的執行出現了錯誤。這不是我想要的。
在網上能夠找到關於動態加載的一些說明,例如:
Opera/Firefox(老版本)下:腳本執行的順序與節點被插入頁面的順序一致
IE/Safari/Chrome下:執行順序沒法獲得保證
注意:
新版本的Firefox下,腳本執行的順序與插入頁面的順序不必定一致,但可經過將script標籤的async屬性設置爲false來保證順序執行 老版本的Chrome下,腳本執行的順序與插入頁面的順序不必定一致,但可經過將script標籤的async屬性設置爲false來保證順序執行
真夠亂的!!(這段描述來自:LABJS源碼淺析。)
爲了解決咱們遇到的問題,咱們能夠在loadScript函數中修改script對象async的值:
var loadScript = function(url) { var script = document.createElement('script'); script.async = false; // 這裏 script.setAttribute('src', url+'?'+'time='+Date.parse(new Date())); document.body.appendChild(script); };
瀏覽器打開,發現能夠正常執行!惋惜該方法只在某些瀏覽器的某些版本中有效,沒有通用性。script browser compatibility給出了下面的兼容性列表:
下面探索的方法均可以正確的加載和執行多個腳本,不過有些一樣有兼容性問題(例如Pormise方式)。
能夠認爲絕大部分瀏覽器動態加載腳本的方式以下:
因此咱們的示例中的三個js腳本的加載和執行順序能夠是下面的狀況之一:
jquery
加載並執行,js/your.js
加載並執行,js/my.js
加載並執行。js/your.js
在前,jquery
在後。jquery
和js/your.js
並行加載,按照加載完畢的順序來執行;等jquery
和js/your.js
都執行完畢後,加載並執行js/my.js
。其中,「加載完畢」這是一個事件,瀏覽器的支持監測這個事件。這個事件在IE下是onreadystatechange
,其餘瀏覽器下是onload
。
據此,Loading JavaScript without blocking給出了下面的代碼:
function loadScript(url, callback){ var script = document.createElement("script") script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function(){ callback(); }; } script.src = url; document.body.appendChild(script); }
callback函數能夠是去加載另一個js,不過若是要加載的js文件較多,就成了「回調地獄」(callback hell)。
回調地獄式能夠經過一些模式來解決,例以下面給出的方式2:
Loader = (function() { var load_cursor = 0; var load_queue; var loadFinished = function() { load_cursor ++; if (load_cursor < load_queue.length) { loadScript(); } } function loadError (oError) { console.error("The script " + oError.target.src + " is not accessible."); } var loadScript = function() { var url = load_queue[load_cursor]; var script = document.createElement('script'); script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; loadFinished(); } }; } else { //Others script.onload = function(){ loadFinished(); }; } script.onerror = loadError; script.src = url+'?'+'time='+Date.parse(new Date()); document.body.appendChild(script); }; var loadMultiScript = function(url_array) { load_cursor = 0; load_queue = url_array; loadScript(); } return { load: loadMultiScript, }; })(); // end Loader //loading ... Loader.load([ 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', './js/your.js', './js/my.js' ]);
load_queue
是一個隊列,保存須要依次加載的js的url。當一個js加載完畢後,load_cursor++
用來模擬出隊操做,而後加載下一個腳本。
onerror事件也添加了回調,用來處理沒法加載的js文件。當遇到沒法加載的js文件時中止加載,剩下的文件也不會加載了。
效果以下:
方式2是串行的去加載,咱們稍加改進,讓能夠並行加載的js腳本儘量地並行加載。
Loader = (function() { var group_queue; // group list var group_cursor = 0; // current group cursor var current_group_finished = 0; var loadFinished = function() { current_group_finished ++; if (current_group_finished == group_queue[group_cursor].length) { next_group(); loadGroup(); } }; var next_group = function() { current_group_finished = 0; group_cursor ++; }; var loadError = function(oError) { console.error("The script " + oError.target.src + " is not accessible."); }; var loadScript = function(url) { console.log("load "+url); var script = document.createElement('script'); script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; loadFinished(); } }; } else { //Others script.onload = function(){ loadFinished(); }; } script.onerror = loadError; script.src = url+'?'+'time='+Date.parse(new Date()); document.body.appendChild(script); }; var loadGroup = function() { if (group_cursor >= group_queue.length) return; current_group_finished = 0; for (var idx=0; idx < group_queue[group_cursor].length; idx++) { loadScript(group_queue[group_cursor][idx]); } }; var loadMultiGroup = function(url_groups) { group_cursor = 0; group_queue = url_groups; loadGroup(); } return { load: loadMultiGroup, }; })(); // end Loader //loading var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', your = './js/your.js', my = './js/my.js' ; Loader.load([ [jquery, your], [my] ]);
Loader.load([ [jquery, your], [my] ]);
表明着jquery
和js/your.js
先儘量快地加載和執行,等它們執行結束後,加載並執行./js/my.js
。
這裏將每一個子數組裏的全部url當作一個group,group內部的腳本儘量並行加載並執行,group之間則爲串行。
這段代碼裏使用了一個計數器current_group_finished
記錄當前group中完成的url的數量,在這個數量和url的總數一致時,進入下一個group。
效果以下:
該方式是對方式3中代碼的重構。
Loader = (function() { var group_queue = []; // group list var current_group_finished = 0; var finish_callback; var finish_context; var loadFinished = function() { current_group_finished ++; if (current_group_finished == group_queue[0].length) { next_group(); loadGroup(); } }; var next_group = function() { group_queue.shift(); }; var loadError = function(oError) { console.error("The script " + oError.target.src + " is not accessible."); }; var loadScript = function(url) { console.log("load "+url); var script = document.createElement('script'); script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; loadFinished(); } }; } else { //Others script.onload = function(){ loadFinished(); }; } script.onerror = loadError; script.src = url+'?'+'time='+Date.parse(new Date()); document.body.appendChild(script); }; var loadGroup = function() { if (group_queue.length == 0) { finish_callback.call(finish_context); return; } current_group_finished = 0; for (var idx=0; idx < group_queue[0].length; idx++) { loadScript(group_queue[0][idx]); } }; var addGroup = function(url_array) { if (url_array.length > 0) { group_queue.push(url_array); } }; var fire = function(callback, context) { finish_callback = callback || function() {}; finish_context = context || {}; loadGroup(); }; var instanceAPI = { load : function() { addGroup([].slice.call(arguments)); return instanceAPI; }, done : fire, }; return instanceAPI; })(); // end Loader //loading var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', your = './js/your.js', my = './js/my.js' ; // Loader.load(jquery, your).load(my).done(); Loader.load(jquery, your) .load(my) .done(function(){console.log(this.msg)}, {msg: 'finished'});
在調用屢次load()函數後,必須調用done()函數。done()函數用來觸發全部腳本的load。
這個方式是對方式4的重寫。改進爲調用load()時候儘量去觸發實際的load操做。
// 這裏調試用的代碼我沒有刪除 Loader = (function() { var group_queue = []; // group list //// url_item = {url:str, start: false, finished:false} // 用於調試 var log = function(msg) { return; console.log(msg); } var isFunc = function(obj) { return Object.prototype.toString.call(obj) == "[object Function]"; } var isArray = function(obj) { return Object.prototype.toString.call(obj) == "[object Array]"; } var isAllStart = function(url_items) { for (var idx=0; idx<url_items.length; ++idx) { if (url_items[idx].start == false ) return false; } return true; } var isAnyStart = function(url_items) { for (var idx=0; idx<url_items.length; ++idx) { if (url_items[idx].start == true ) return true; } return false; } var isAllFinished = function(url_items) { for (var idx=0; idx<url_items.length; ++idx) { if (url_items[idx].finished == false ) return false; } return true; } var isAnyFinished = function(url_items) { for (var idx=0; idx<url_items.length; ++idx) { if (url_items[idx].finished == true ) return true; } return false; } var loadFinished = function() { nextGroup(); }; var showGroupInfo = function() { for (var idx=0; idx<group_queue.length; idx++) { group = group_queue[idx]; if (isArray(group)) { log('**********************'); for (var i=0; i<group.length; i++) { log('url: '+group[i].url); log('start: '+group[i].start); log('finished:'+group[i].finished); log('-------------------'); } log('isAllStart: ' + isAllStart(group)); log('isAnyStart: ' + isAnyStart(group)); log('isAllFinished: ' + isAllFinished(group)); log('isAnyFinished: ' + isAnyFinished(group)); log('**********************'); } } }; var nextGroup = function() { while (group_queue.length > 0) { showGroupInfo(); // is Func if (isFunc(group_queue[0])) { log('## nextGroup: exec func'); group_queue[0](); // exec group_queue.shift(); continue; // is Array } else if (isAllFinished(group_queue[0])) { log('## current group all finished'); group_queue.shift(); continue; } else if (!isAnyStart(group_queue[0])) { log('## current group no one start!'); loadGroup(); break; } else { break; } } }; var loadError = function(oError) { console.error("The script " + oError.target.src + " is not accessible."); }; var loadScript = function(url_item) { log("load "+url_item.url); url = url_item.url; url_item.start = true; var script = document.createElement('script'); script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; url_item.finished = true; loadFinished(); } }; } else { //Others script.onload = function(){ url_item.finished = true; loadFinished(); }; } script.onerror = loadError; script.src = url+'?'+'time='+Date.parse(new Date()); document.body.appendChild(script); }; var loadGroup = function() { for (var idx=0; idx < group_queue[0].length; idx++) { loadScript(group_queue[0][idx]); } }; var addGroup = function(url_array) { log('add :' + url_array); if (url_array.length > 0) { group = []; for (var idx=0; idx<url_array.length; idx++) { url_item = { url: url_array[idx], start: false, finished: false, }; group.push(url_item); } group_queue.push(group); } nextGroup(); }; var addFunc = function(callback) { callback && isFunc(callback) && group_queue.push(callback); log(group_queue); nextGroup(); }; var instanceAPI = { load : function() { addGroup([].slice.call(arguments)); return instanceAPI; }, wait : function(callback) { addFunc(callback); return instanceAPI; } }; return instanceAPI; })(); // end Loader,這尼瑪就是一個狀態機 // loading var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', your = './js/your.js', my = './js/my.js' ; // Loader.load(jquery, your).load(my); Loader.load(jquery, your) .wait(function(){console.log("yeah, jquery and your.js were loaded")}) .load(my) .wait(function(){console.log("yeah, my.js was loaded")});
上面的調用中,每次load時候會嘗試立刻加載和執行這些腳本,而不是像方式4那樣要等done()被調用。
另外出現了新的函數wait,當wait以前的load和wait執行結束後,該wait中的匿名函數會被調用。
效果以下:
Promise是一種設計模式。關於Promise,下面的幾篇文章值得一看:
當前瀏覽器對Promise的支持狀況以下:
使用Promise解決腳本動態加載問題的方案以下:
function getJS(url) { return new Promise(function(resolve, reject) { var script = document.createElement('script'); script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; resolve('success: '+url); } }; } else { //Others script.onload = function(){ resolve('success: '+url); }; } script.onerror = function() { reject(Error(url + 'load error!')); }; script.src = url+'?'+'time='+Date.parse(new Date()); document.body.appendChild(script); }); } //loading var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', your = './js/your.js', my = './js/my.js' ; getJS(jquery).then(function(msg){ return getJS(your); }).then(function(msg){ return getJS(my); }).then(function(msg){ console.log(msg); });
這個實現中js是串行加載的。
效果以下:
可使用Promise.all使jquery
和js/your.js
並行加載。
Promise.all([getJS(jquery), getJS(your)]).then(function(results){ return getJS(my); }).then(function(msg){ console.log(msg); });
Promise配合生成器(Generator)可讓js程序按照串行的思惟編寫。
關於生成器,下面的幾篇文章值得一看:
瀏覽器的支持狀況以下:
來兩個典型的生成器示例:
示例1:
function *addGenerator() { var i = 0; while (true) { i += yield i; } } var adder = addGenerator(); console.log( adder.next().value ); // yield i時候暫停 (循環1) console.log( adder.next(5).value ); // 循環1中yield i的結果爲5,i+=5,進入下一個循環(循環2),循環2中yield i 暫停,返回5 console.log( adder.next(5).value ); // 循環2中yield i的結果爲5 console.log( adder.next(5).value ); // 循環3中yield i的結果爲5 console.log( adder.next(50).value ); //循環4中yield i的結果爲50,i+=50,進入循環6
輸出:
0 5 10 15 65
示例2:
function* idMaker(){ var index = 0; while(index < 3) yield index++; } var gen = idMaker(); while ( result = gen.next() ) { if (!result.done) { console.log(result.done + ':' + result.value); } else{ console.log(result.done + ':' + result.value); break; } }
輸出:
false:0 false:1 false:2 true:undefined
下面的文章介紹瞭如何搭配Promise和Generator:
Generator+Promise實現js腳本動態加載的方式以下:
function getJS(url) { return new Promise(function(resolve, reject) { var script = document.createElement('script'); script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; resolve('success: '+url); } }; } else { //Others script.onload = function() { resolve('success: '+url); }; } script.onerror = function() { reject(Error(url + 'load error!')); }; script.src = url+'?'+'time='+Date.parse(new Date()); document.body.appendChild(script); }); } function spawn(generatorFunc) { function continuer(verb, arg) { var result; try { result = generator[verb](arg); // 這個result是生成器的返回值,有value和done兩個屬性 } catch (err) { return Promise.reject(err); } if (result.done) { return result.value; } else { return Promise.resolve(result.value).then(onFulfilled, onRejected); // result.value是promise對象 } } var generator = generatorFunc(); var onFulfilled = continuer.bind(continuer, "next"); var onRejected = continuer.bind(continuer, "throw"); return onFulfilled(); } //// loading var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', your = './js/your.js', my = './js/my.js' ; // 「串行」代碼在這裏 spawn(function*() { try { yield getJS(jquery); console.log('jquery has loaded'); yield getJS(your); console.log('your.js has loaded'); yield getJS(my); console.log('my.js has loaded'); } catch (err) { console.log(err); } });
效果以下:
在For Your Script Loading Needs列出了許多工具,例如lazyload、LABjs、RequireJS等。
有些工具也提供了新的思路,例如LABjs中可使用ajax獲取同域下的js文件。
Dynamic Script Execution Order