Skip to content

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

Open
@huruji

Description

@huruji
Owner

dsc00051

对于AMD规范的具体描述在这里可以找到AMD (中文版). AMD规范作为JavaScript模块加载的最流行的规范之一,已经有很多的实现了,我们就来实现一个最简单的AMD加载器

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

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

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。

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

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

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,内容仅仅为输出内容,如下:

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

我们使得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

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

我们紧接着来拓展一下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

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

接下来我们继续往下走,我们上面的实现是基于一个模块只会被一个模块依赖的,如果被多个模块依赖的时候我们需要防止的是这个被依赖的模块中的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

结果符合我们预期。

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

Activity

changed the title [-]动手实现一个AMD模块加载器(未完待续)[/-] [+]动手实现一个AMD模块加载器(一)[/+] on Oct 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @huruji

        Issue actions

          动手实现一个AMD模块加载器(一) · Issue #13 · huruji/blog