動手實現一個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模塊加載器(三)

相關文章
相關標籤/搜索