动手实现一个AMD模块加载器(一)

对于AMD规范的具体描述在这里能够找到AMD (中文版)). AMD规范做为JavaScript模块加载的最流行的规范之一,已经有不少的实现了,咱们就来实现一个最简单的AMD加载器javascript

首先咱们须要明白咱们须要有一个全部模块的入口也就是主模块,主模块的依赖加载的过程当中迭代加载相应的依赖,咱们使用use方法来加载使用主模块。
同时咱们须要明白加载依赖以后须要执行模块的方法,这显然应该使用callback,同时为了多个模块依赖同一个模块的时候,不会屡次执行这个模块咱们应该判断这个模块是否已经加载过,所以咱们可使用一个对象来描述一个模块。而全部的模块咱们能够一个对象来存储,使用模块名做为属性名来区分不一样模块。java

首先咱们先来实现use方法,这个方法就是主模块方法,使用这个模块的方法就是加载依赖以后,执行主模块的方法,以下:node

function use(deps, callback) {
  if(deps.length === 0) {
    callback();
  }
  var depsLength = deps.length;
  var params = [];
  for(var i = 0; i < deps.length; i++) {
    (function(j){
      loadMod(deps[j], function(param) {
        depsLength--;
        params[j] = param;
        if(depsLength === 0) {
          callback.apply(null, params);
        }
      })
    })(i)
  }
}复制代码

说明一下loadMod方法为加载依赖的方法,其中由于主模块加载了这些模块以后是须要做为callback的参数来使用这些模块的,所以咱们既须要判断是否加载完毕,也须要将这些模块做为参数传递给主模块的callback。git

接下来咱们来实现这个loadMod方法,为了一步一步实现功能,咱们假设这里全部的模块都没有依赖其余模块,只有主模块依赖,所以这个时候loadMod方法作的事情就是建立script并将相应的文件加载进来,这里咱们再次假设全部模块名和文件名一致,而且全部的js文件路径与页面文件路径一致。github

这个过程当中咱们须要知道这个script的确是加载了才执行callback,所以须要使用事件进行监听,因此有如下代码api

function loadMod(name, callback) {
  var doc = document;
  var node = doc.createElement('script');
  node.charset = 'utf-8';
  node.src = name + '.js';
  node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
  doc.body.appendChild(node);
  if('onload' in node) {
    node.onload = callback;
  } else {
    node.onreadystatechange = function() {
      if(node.readyState === 'complete') {
        callback();
      }
    }
  }
}复制代码

接着咱们须要来实现最为核心的define函数,这个函数的目的是定义模块,为了简便避免作类型判断,咱们暂时规定全部的模块都必须定义模块名,不容许匿名模块的使用,而且咱们先暂且假设这里没有模块依赖。以下:数组

var modMap = [];
function define(name, callback) {
  modMap[name] = {};
  modMap[name].callback = callback;
}复制代码

这时咱们发现一个问题这样定义的模块内部的方法并无被调用并且模块返回的参数也没有传递给主模块上,所以在loadMod的过程当中咱们应该再次使用use方法,只不过此时依赖为一个空数组,所以咱们能够将loadMod方法再次抽离出一个loadScript方法来,以下:浏览器

function loadMod(name, callback) {
    use([], function() {
      loadscript(name, callback);
    })
  }


  function loadscript(name, callback) {
    var doc = document;
    var node = doc.createElement('script');
    node.charset = 'utf-8';
    node.src = name + '.js';
    node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
    doc.body.appendChild(node);
    node.onload = function() {
      var param = modMap[name].callback();
      callback(param);
    }
  }复制代码

这个时候咱们先无论功能是否实现,而是能够发现如今这个代码的全局变量实在太多,所以咱们须要简单封装一下,以下:缓存

(function(root){
  var modMap = [];

  function use(deps, callback) {
    if(deps.length === 0) {
      callback();
    }
    var depsLength = deps.length;
    var params = [];
    for(var i = 0; i < deps.length; i++) {
      (function(j){
        loadMod(deps[j], function(param) {
          depsLength--;
          params[j] = param;
          if(depsLength === 0) {
            callback.apply(null, params);
          }
        })
      })(i)
    }
  }

  function loadMod(name, callback) {
    use([], function() {
      loadscript(name, callback);
    })
  }


  function loadscript(name, callback) {
    var doc = document;
    var node = doc.createElement('script');
    node.charset = 'utf-8';
    node.src = name + '.js';
    node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
    doc.body.appendChild(node);
    node.onload = function() {
      var param = modMap[name].callback();
      callback(param);
    }
  }

  function define(name, callback) {
    modMap[name] = {};
    modMap[name].callback = callback;
  }

  var loadjs = {
    define: define,
    use: use
  };

  root.define = define;
  root.loadjs = loadjs;
  root.modMap = modMap;
})(window);复制代码

这个时候咱们简单使用一下,咱们在同级路径下新建a.js和b.js,内容仅仅为输出内容,以下:app

define('a', function() {
  console.log('a');
});复制代码
define('b', function() {
  console.log('b');
});复制代码

使用主模块以下:

loadjs.use(['a','b'], function(a, b) {
   console.log('main');
})复制代码

这个时候咱们打开浏览器能够发现a,b,main依次被打印出来了,以下:

1
1

咱们使得a.js和b.js更复杂一些,能够放回方法,以下

define('a', function() {
  console.log('a');
  return {
    add: function(a, b) {
      return a + b;
    }
  }
});复制代码
define('b', function() {
  console.log('b');
  return {
    equil: function(a,b) {
      return a===b;
    }
  }
});复制代码
loadjs.use(['a','b'], function(a, b) {
      console.log('main');
      console.log(a.add(1,2));
      console.log(b.equil(1,2));
})复制代码

这个时候咱们打开浏览器能够发现是正常输出的,以下:

2
2

这也就是说咱们的功能目前来讲是可用的。

咱们紧接着来拓展一下define方法,目前来讲是不支持依赖的,其实基本上来讲是不可用的,那么接下来咱们来拓展一下使得支持依赖.
遵循由简到繁的原则,咱们先暂定全部的依赖都是独立的,也就是说咱们先认为,一个模块不会被超过两个模块依赖,也就是说咱们此时应该loadMod函数中同时去解析是否有依赖。

咱们先修改一下最简单的define方法,只须要增长一下依赖属性便可,以下:

function define(name, deps, callback) {
    modMap[name] = {};
    modMap[name].deps = deps;
    modMap[name].callback = callback;
  }复制代码

接下来咱们考虑一下loadMod方法,前面咱们很是简单就是在这里调用了脚本加载的函数,如今模块会对其余模块进行依赖了,因此咱们在这里必需要调用use方法,而且这个模块的依赖属性做为第一个参数,所以在这以前咱们必须先使用loadscript方法来确保脚本已经加载完毕,因此大体修改以下:

function loadMod(name, callback) {
    loadscript(name, function() {
      use(modMap[name].deps, function() {

      })
    });
  }复制代码

接着考虑一下loadscript方法,以前的loadscript方法加载完毕脚本以后执行了主模块的回调函数,然而目前loadscript方法的回调是一个对use方法的封装,所以直接执行callback就好了,修改成以下:

function loadscript(name, callback) {
    var doc = document;
    var node = doc.createElement('script');
    node.charset = 'utf-8';
    node.src = name + '.js';
    node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
    doc.body.appendChild(node);
    node.onload = function() {
      callback();
    }
  }复制代码

接下来咱们再考虑一下如何可以将一个模块的返回值传递给依赖他的模块,按照以前的思路主模块中咱们使用一个回调函数,最后这个arguments是在loadscript中传递进去的,而现现在咱们在loadMod方法和use方法有了循环调用,因此咱们应该给最后一个没有依赖的函数一个出口,同时须要调用loadMod方法的callback方法,因此咱们单独抽离一个execMod方法,以下:

function execMod(name, callback, params) {
    var exp = modMap[name].callback.apply(null, params);
    callback(exp);
  }复制代码

在loadMod方法中调用这个方法便可,以下:

function loadMod(name, callback) {
    loadscript(name, function() {
      use(modMap[name].deps, function() {
        execMod(name, callback, Array.prototype.slice.call(arguments, 0));
      })
    });
  }复制代码

这里须要理解的是arguments,看似这个arguments为空,可是咱们注意到咱们以前已经在use方法中使用了apply方法将参数传递进来了,因此arguments就是相应的依赖,
此时整个内容以下:

(function(root){
  var modMap = [];
  function use(deps, callback) {
    if(deps.length === 0) {
      callback();
    }
    var depsLength = deps.length;
    var params = [];
    for(var i = 0; i < deps.length; i++) {
      (function(j){
        loadMod(deps[j], function(param) {
          depsLength--;
          params[j] = param;
          if(depsLength === 0) {
            callback.apply(null, params);
          }
        })
      })(i)
    }
  }

  function loadMod(name, callback) {
    loadscript(name, function() {
      use(modMap[name].deps, function() {
        execMod(name, callback, Array.prototype.slice.call(arguments, 0));
      })
    });
  }

  function execMod(name, callback, params) {
    console.log(callback);
    var exp = modMap[name].callback.apply(null, params);
    callback(exp);
  }

  function loadscript(name, callback) {
    var doc = document;
    var node = doc.createElement('script');
    node.charset = 'utf-8';
    node.src = name + '.js';
    node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
    doc.body.appendChild(node);
    node.onload = function() {
      callback();
    }
  }

  function define(name, deps, callback) {
    modMap[name] = {};
    modMap[name].deps = deps;
    modMap[name].callback = callback;
  }

  var loadjs = {
    define: define,
    use: use
  };

  root.define = define;
  root.loadjs = loadjs;
  root.modMap = modMap;
})(window);复制代码

此时咱们再次作一个测试,以下:

loadjs.use(['a'], function(a) {
      console.log('main');
      console.log(a.add(1,2));
    })复制代码
define('a', ['b'], function(b) {
  console.log('a');
  console.log(b.equil(1,2));
  return {
    add: function(a, b) {
      return a + b;
    }
  }
});复制代码
define('b', ['c'], function(c) {
  console.log('b');
  console.log(c.sqrt(4));
  return {
    equil: function(a,b) {
      return a===b;
    }
  }
});复制代码
define('c', [], function() {
  console.log('c');
  return {
    sqrt: function(a) {
      return Math.sqrt(a)
    }
  }
});复制代码

此时运行结果以下:

3
3

结果正确,说明咱们的实现是正确的。

接下来咱们继续往下走,咱们上面的实现是基于一个模块只会被一个模块依赖的,若是被多个模块依赖的时候咱们须要防止的是这个被依赖的模块中的callback被屡次调用,所以咱们能够对每一个模块使用一个loaded属性来标识出这个模块是否已经加载。

将define函数修改成如下内容:

function define(name, deps, callback) {
    modMap[name] = {};
    modMap[name].deps = deps;
    modMap[name].loaded = true;
    modMap[name].callback = callback;
  }复制代码

咱们须要知道的是咱们能够经过判断modMap中是否有相应的模块来判断是否模块加载,可是若是加载完毕再次使用use方法,则会再次执行该模块的代码,这是不对的,所以咱们须要将每一个模块的exports缓存起来,以便咱们再次调用。同时咱们思考一下一个模块在加载的过程当中,会有几种状态呢?

可想而知,大概能够分为没有load、loading中、load完毕但代码没有执行完成、代码执行完成这几种状态,一样能够用属性来标识出。

没有load则执行loadscript方法、loading中则能够将callback推到一个数组中,等到loaded和代码执行完毕以后执行,而load完毕代码未执行完则执行代码,所以咱们能够开始进行修改。

先修改define函数以下:

function define(name, deps, callback) {
    modMap[name] = modMap[name] || {};
    modMap[name].deps = deps;
    modMap[name].status = 'loaded';
    modMap[name].callback = callback;
    modMap[name].oncomplete = modMap[name].oncomplete || [];
  }复制代码

将loadMod方法修改以下:

function loadMod(name, callback) {
    console.log('modMap', modMap);
    if(!modMap[name]) {
      modMap[name] = {
        status: 'loading',
        oncomplete: []
      };
      console.log('initloading');
      loadscript(name, function() {
        use(modMap[name].deps, function() {
          execMod(name, callback, Array.prototype.slice.call(arguments, 0));
        })
      });
    } else if(modMap[name].status === 'loading') {
      modMap[name].oncomplete.push(callback);
    } else if (!modMap[name].exports){
      use(modMap[name].deps, function() {
        execMod(name, callback, Array.prototype.slice.call(arguments, 0));
      })
    }else {
      callback(modMap[name].exports);
    }
  }复制代码

代码执行完毕以后将结果添加到每一个模块的exports中,同时须要执行oncomplete数组中的函数,因此将execmod修改成如下:

function execMod(name, callback, params) {
    var exp = modMap[name].callback.apply(null, params);
    modMap[name].exports = exp;
    callback(exp);
    execComplete(name);
  }复制代码

添加execComplete方法,以下:

function execComplete(name) {
    for(var i = 0; i < modMap[name].oncomplete.length; i++) {
      modMap[name].oncomplete[i](modMap[name].exports);
    }
  }复制代码

此时整个代码以下:

(function(root){
  var modMap = {};

  function use(deps, callback) {
    if(deps.length === 0) {
      callback();
    }
    var depsLength = deps.length;
    var params = [];
    for(var i = 0; i < deps.length; i++) {
      (function(j){
        loadMod(deps[j], function(param) {
          depsLength--;
          params[j] = param;
          if(depsLength === 0) {
            callback.apply(null, params);
          }
        })
      })(i)
    }
  }

  function loadMod(name, callback) {
    console.log('modMap', modMap);
    if(!modMap[name]) {
      modMap[name] = {
        status: 'loading',
        oncomplete: []
      };
      console.log('initloading');
      loadscript(name, function() {
        use(modMap[name].deps, function() {
          execMod(name, callback, Array.prototype.slice.call(arguments, 0));
        })
      });
    } else if(modMap[name].status === 'loading') {
      modMap[name].oncomplete.push(callback);
    } else if (!modMap[name].exports){
      use(modMap[name].deps, function() {
        execMod(name, callback, Array.prototype.slice.call(arguments, 0));
      })
    }else {
      callback(modMap[name].exports);
    }
  }

  function execMod(name, callback, params) {
    var exp = modMap[name].callback.apply(null, params);
    modMap[name].exports = exp;
    callback(exp);
    execComplete(name);
  }

  function execComplete(name) {
    for(var i = 0; i < modMap[name].oncomplete.length; i++) {
      modMap[name].oncomplete[i](modMap[name].exports);
    }
  }
  function loadscript(name, callback) {
    var doc = document;
    var node = doc.createElement('script');
    node.charset = 'utf-8';
    node.src = name + '.js';
    node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
    doc.body.appendChild(node);
    node.onload = function() {
      callback();
    }
  }

  function define(name, deps, callback) {
    modMap[name] = modMap[name] || {};
    modMap[name].deps = deps;
    modMap[name].status = 'loaded';
    modMap[name].callback = callback;
    modMap[name].oncomplete = modMap[name].oncomplete || [];
  }

  var loadjs = {
    define: define,
    use: use
  };

  root.define = define;
  root.loadjs = loadjs;
  root.modMap = modMap;
})(window);复制代码

一样,再次进行测试,以下:

loadjs.use(['a', 'b'], function(a, b) {
      console.log('main');
      console.log(b.equil(1,2));
      console.log(a.add(1,2));
    })复制代码
define('a', ['c'], function(c) {
  console.log('a');
  console.log(c.sqrt(4));
  return {
    add: function(a, b) {
      return a + b;
    }
  }
});复制代码
define('b', ['c'], function(c) {
  console.log('b');
  console.log(c.sqrt(9));
  return {
    equil: function(a,b) {
      return a===b;
    }
  }
});复制代码
define('c', [], function() {
  console.log('c');
  return {
    sqrt: function(a) {
      return Math.sqrt(a)
    }
  }
});复制代码

此时结果输出以下:

4
4

结果符合咱们预期。

系列文章:
动手实现一个AMD模块加载器(一)
动手实现一个AMD模块加载器(二)
动手实现一个AMD模块加载器(三)

相关文章
相关标签/搜索