实现异步加载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其内部也是用类似的方法实现监控文件的加载情况的。还想写点什么,一想到马上就要回家过年了,算了结尾收工吧!

posted on 2016-07-12 22:28  阿牛的哥  阅读(8384)  评论(0编辑  收藏  举报

导航