動手實現一個AMD模組載入器(一)

灰風GreyWind發表於2017-11-04

對於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
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模組載入器(三)

相關文章