實現異步加載js文件及加載完成后回調


模塊化工具類實現方式

基於AMD、CMD模式的JS模塊化管理工具越來越流行,這些工具通常只需在頁面中加載對應的工具JS,其他JS文件都是異步加載的,比如RequireJS就可以象下面這樣做。

首先在頁面加載

<script data-main="scripts/main.js" src="scripts/require.js"></script>

然后工具會自動識別data-main屬性值,並加載對應的JS文件,在main.js可以加載更多模塊來實現復雜的業務。這里僅加載一個jQuery模塊

1
2
3
4
5
6
7
8
require.config({
     paths: {
         'jquery' : 'lib/jquery-1.11.1.min'
     }
});
require([ 'jquery' ], function ($) {
     console.log( 'jQuery模塊文件已經加載完成' );
});

異步加載“業務類”JS文件

前人種樹,后人乘涼,牛人們開發了各種好用的工具,我們可以直接拿來用,但是我們至少應該理解其最基本原理,下面讓我們一步步實現自己的JS文件異步加載工具。之所以叫“業務類”,就是那些JS不提供依賴或者說是不提供任何功能,只負責實現自己的業務。比如一個1.js文件內容如下:

alert('1.js文件加載了');

下面需要在頁面中加載這個JS,通常使用如下方法:

1
2
3
4
var script = document.createElement( 'script' );
var head = document.getElementsByTagName( 'head' )[0];
script.src = '1.js' ;
head.appendChild(script);

點擊查看demo,可以看出異步加載這類JS比較簡單,因為只管添加到head中讓瀏覽器加載,至於什么時候加載完,我們不用管,因為其他JS不會依賴該文件,所以不需要關注他是否加載完成。

異步加載JS文件並執行回調函數

先把上面的代碼簡單封裝成一個函數,因為需要執行回調所以多加一個參數

1
2
3
4
5
6
7
8
9
10
function loadJS(src, callback){
     var script = document.createElement( 'script' );
     var head = document.getElementsByTagName( 'head' )[0];
     script.src = src;
     head.appendChild(script);
     callback();
}
loadJS( '1.js' , function (){
     alert( '執行回調' );
});

按上面代碼的流程head.appendChild(script)加載腳本之后執行回調callback(),可以點擊查看demo,打開頁面后依次彈出'執行回調'、“'1.js文件加載了'”,並不是我想所預想的先加載1.js然后執行里面的代碼最后再執行回調函數,這個問題主要因為異步加載引起的,在head.appendChild(script)之后,引擎並不會等待文件的加載和執行完成,就繼續往下執行了。對於上面的代碼這個問題並不會致命,但對於下面所說在這種情況就會引起報錯。

異步加載“依賴類”JS文件

所謂依賴類”,就是異步加載的JS文件會提供依賴或者說是提供功能,比如一個2.js文件內容如下:

1
2
3
function sayHello(){
     alert( 'hello' );
}

這個JS文件中包含了一個函數,而我在頁面中想通過loadJS加載2.js文件,這樣就可以使用這個函數了。

1
2
3
loadJS( '2.js' , function (){
     sayHello();
});

但由於是異步加載的,執行回調里的sayHello()時,2.js文件可能還沒加載完成或者未執行,所以會報錯“sayHello is not defined”,點擊這里查看demo,看來還要給loadJS函數增加一個監聽文件加載狀態的功能,如果加載完成就執行回調。增加之前先看了下jQuery的處理方法,以下為jQuery源碼中jQuery.ajaxTransport( "script"的回調代碼片斷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
script = document.createElement( "script" );
 
script.async = true ;
 
if ( s.scriptCharset ) {
     script.charset = s.scriptCharset;
}
 
script.src = s.url;
 
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function ( _, isAbort ) {
 
     if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
 
         // Handle memory leak in IE
         script.onload = script.onreadystatechange = null ;
 
         // Remove the script
         if ( script.parentNode ) {
             script.parentNode.removeChild( script );
         }
 
         // Dereference the script
         script = null ;
 
         // Callback if not abort
         if ( !isAbort ) {
             callback( 200, "success" );
         }
     }
};
 
// Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
// Use native DOM manipulation to avoid our domManip AJAX trickery
head.insertBefore( script, head.firstChild );

發現jQuery是通過script元素的onload或者onreadystatechange事件來實現回調的。readyState和onreadystatechange屬性都是IE特有的,火狐、Chrome等一些現代瀏覽器都沒有取而代之的是onload事件,反之IE也沒有onload事件,所以這里onload和onreadystatechange綁定了同一個處理函數。下面開始畫瓢:

1
2
3
4
5
6
7
8
9
10
11
12
13
function loadJS(src, callback){
     var script = document.createElement( 'script' );
     var head = document.getElementsByTagName( 'head' )[0];
     script.src = src;
     head.appendChild(script);
     if ( typeof callback === 'function' ){
         script.onload = script.onreadystatechange = function (){
             if (!script.readyState || /loaded|complete/.test(script.readyState)){
                 callback();
             }
         }
     }
}

查看demo,上面的代碼不會再報錯了,但測試發現IE9及以上會彈出兩次hello,查了一下得知從IE9開始支持onload事件了但他同時還支持onreadystatechange,因為這里綁定了兩個所以就執行了兩次,解決的方法可以分開綁定或者加一個標識判斷是否已經執行過回調了,個人建議使用分開綁定的方式解決,RequireJS 2.1.11也是采用的這種方式。

下面是用標識處理執行兩次問題的demo,加載JS的代碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function loadJS(src, callback){
     var script = document.createElement( 'script' );
     var head = document.getElementsByTagName( 'head' )[0];
     var loaded;
     script.src = src;
     if ( typeof callback === 'function' ){
         script.onload = script.onreadystatechange = function (){
             if (!loaded && (!script.readyState || /loaded|complete/.test(script.readyState))){
                 script.onload = script.onreadystatechange = null ;
                 loaded = true ;
                 callback();
             }
         }
     }
     head.appendChild(script);
}

看起來文章挺長內容挺多,其實就是一個異步加載的函數,也沒有啥技術含量,文章開頭所寫的插件RequireJS其內部也是用類似的方法實現監控文件的加載情況的。還想寫點什么,一想到馬上就要回家過年了,算了結尾收工吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM