AMD模組載入器
模組載入對於前端來說是非常重要的一個知識點.當前的主流模組載入方式有AMD
,CMD
和CommonJs
(node
環境下)。AMD
和CMD
有很多共同之處例如都是檔案提前並行載入,依賴載入完成通知模組,模組監聽依賴是否全部執行完畢,如果直接執行factory
.核心原理區別其實並不大,只不過程式碼的執行順序有所區別而已。AMD
是提前執行,CMD
是按需執行.而CMD
和CommonJs
的區別在於檔案的載入順序不同,CommonJS
檔案是就近載入,程式碼就近執行.這裡我用最少的程式碼來實現一個AMD
模組載入器以便大家來真正理解掌握前端的模組載入
fw.js
不廢話,直接上程式碼。重要的程式碼塊我已寫上註釋
var fwjs, require, define;
(function (global) {
var req,
ob = Object.prototype,
toString = ob.toString,
hasOwn = ob.hasOwnProperty,
version = "1.0.0",
contexts = {},
head = document.getElementsByTagName('head')[0],
globalDefQueue = [],
defContextName ="_";
function isFunction(f) {
return toString.call(f) == "[object Function]"
}
function isArray(arr) {
return toString.call(arr) == "[object Array]"
}
function getOwn(obj, prop) {
return hasOwn.call(obj, prop) && obj[prop];
}
// 建立script節點
function createScriptNode(){
var node = document.createElement('script');
node.type = 'text/javascript';
node.charset = 'utf-8';
node.async = true;
return node;
};
// 載入js檔案
function loadScript(fn, moduleName, url){
var node = createScriptNode();
node.setAttribute('data-fwmodule', moduleName);
node.addEventListener('load', fn, false);
node.src = url;
head.appendChild(node)
}
function createContext(){
var context = {},
registry = {},
undefEvents = {},
defined = {},
urlLoaded = {},
defQueue=[],
requireCounter = 1
;
function makeModuleMap(name, parentModuleMap){
var isDefine = true,
normalizedName = "",
originalName = name,
url = name,
parentName = parentModuleMap ? parentModuleMap.name : "";
if (!name) {
isDefine = false;
name = 'fw' + (requireCounter += 1);
}
// 在這裡並沒有對id和name進行處理 主要是不支援config
return {
name: name,
parentMap: parentModuleMap,
url: name,
originalName: originalName,
isDefine: isDefine,
id: name
};
}
function getModule(depMap) {
var id = depMap.id,
mod = getOwn(registry, id);
if (!mod) {
mod = registry[id] = new context.Module(depMap);
}
return mod;
}
// 構建Module類
function Module(map){
this.events = getOwn(undefEvents, map.id) || {};
this.map = map;
this.depExports = [];
this.depMaps = [];
this.depMatched = []; // 依賴是否已defined
this.depCount = 0;
}
Module.prototype = {
// 模組初始化
init: function(depMaps, factory){
if (this.inited) {
return;
}
this.factory = factory;
this.inited = true;
this.depMaps = depMaps || [];
this.enable();
},
// 啟用模組
enable:function(){
this.enabled = true;
this.enabling = true;
this.depMaps.forEach(function(depMap, i) {
var mode = null;
if (typeof depMap == "string") {
depMap = makeModuleMap(depMap, this.map.isDefine ? this.map : null);
mod = getOwn(registry, depMap.id);
this.depCount += 1;
this.depMaps[i] = depMap;
var fn = function (depExports) {
if (!this.depMatched[i]) {
this.depMatched[i] = true;
this.depCount -= 1;
this.depExports[i] = depExports;
}
this.check();
}.bind(this)
// 如果模組已經載入過
if (getOwn(defined, depMap.id) && mod.defineEmitComplete) {
fn(defined[depMap.id]);
} else {
mod = getModule(depMap);
// 繫結defined事件,監聽依賴的載入,每一個依賴載入完成,模組都會收集依賴的exports,但所有的依賴載入完畢模組才會執行
mod.on("defined", fn)
}
mod = registry[depMap.id];
if (mod && !mod.enabled) {
mod.enable()
}
}
}.bind(this))
this.enabling = false;
this.check();
},
// 執行模組
check:function(){
if (!this.enabled || this.enabling) {
return;
}
var id = this.map.id,
depExports = this.depExports,
exports = this.exports,
factory = this.factory;
if (!this.inited) {
this.load(); //
} else if (!this.defining){
this.defining = true; // defining下面程式碼每個模組只執行一次
if (isFunction(factory)) { // 模組的factory只允許是函式
// 只有模組的依賴全部執行完,才會執行factory
if (this.depCount < 1 && !this.defined) { // 只有暴露出exports defined屬性才為true
exports = factory.apply(this, depExports)
this.exports = exports;
if (this.map.isDefine) {
defined[id] = exports;
}
this.defined = true;
}
}
this.defining = false;
if (this.defined && !this.defineEmitted) {
this.defineEmitted = true;
this.emit('defined', this.exports);
this.defineEmitComplete = true;
}
}
},
load(){
if (this.loaded) {
return;
}
this.loaded = true;
var url = this.map.url;
//Regular dependency.
if (!urlLoaded[url]) {
urlLoaded[url] = true;
loadScript(context.onScriptLoad, this.map.id, url)
}
},
on: function (name, cb) {
var cbs = this.events[name];
if (!cbs) {
cbs = this.events[name] = [];
}
cbs.push(cb);
},
emit: function(name, data){
var evts = this.events[name] || [];
evts.forEach(function(cb){
cb(data);
})
}
}
// 將globalQueue轉入defQueue
function getGlobalQueue() {
//Push all the globalDefQueue items into the context's defQueue
if (globalDefQueue.length) {
globalDefQueue.forEach(function(queueItem) {
var id = queueItem[0];
if (typeof id === 'string') {
context.defQueueMap[id] = true;
}
defQueue.push(queueItem);
});
globalDefQueue = [];
}
}
context.Module = Module;
context.require = function (deps, callback){
//console.log(deps, callback)
var requireMod = getModule(makeModuleMap(null));
requireMod.init(deps, callback);
};
context.onScriptLoad = function(evt){
if (evt.type == "load") {
var node = evt.currentTarget || evt.srcElement;
node.removeEventListener('load', context.onScriptLoad, false);
var id = node.getAttribute('data-fwmodule')
context.completeLoad(id);
}
};
context.completeLoad = function(moduleName){
var found, args;
// 提取當前載入的模組
getGlobalQueue();
while (defQueue.length) {
args = defQueue.shift();
if (args[0] === null) {
args[0] = moduleName;
if (found) {
break;
}
found = true;
} else if (args[0] === moduleName) {
found = true;
}
if (!getOwn(defined, args[0])) {
// 依賴載入完成之後,對檔案進行初始化
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
}
return context;
}
// 入口的require函式
req = fwjs = require = function(deps, callback) {
var context = {},
contextName = defContextName;
if (isFunction(deps)) {
deps = [];
callback = deps
}
context = getOwn(contexts, contextName);
if (!context){
context = contexts[contextName] = createContext();
}
return context.require(deps, callback)
}
// define只允許匿名模組
define = function(deps, callback){
if (!isArray(deps)) {
callback = deps;
deps = [];
}
var name = null, context;
globalDefQueue.push([name, deps, callback]);
globalDefQueue[name] = true;
}
}(this))
複製程式碼
程式碼為什麼會這麼少?
- 不相容IE
- 不支援
config
(不需要處理大量的複雜的pluigin, shim, baseUrl
之類的全域性配置)- 只支援匿名模組(不需要處理
name
),模組只支援用函式包裹- 僅供學習使用,以理解
AMD
的模組載入原理
實現原理
因為程式碼很少,大家看幾遍就很容易看懂了,所以這個我只是簡單的說下吧
每一個依賴載入執行完都讓depCount
減一併且模組會check
,如果depCount
為0就會執行factory
,factory
執行完繼續向上通知因為模組本身可以是依賴, 直至入口的require
模組執行
demo
index.js
require( ["./src/app.js", "./src/app1.js", "./src/m/app2.js"],
function(a, b, c) {
console.log(a, b, c)
}
);
複製程式碼
app.js
define(function () {
//Do setup work here
return {
name:"app"
}
});
複製程式碼
app1.js
define(function () {
//Do setup work here
return {
name: "app1"
}
});
複製程式碼
m/app2.js
define(["./src/app.js"],function (a) {
console.log(a)
//Do setup work here
return {
name:"app2"
}
});
複製程式碼
展示
RequireJs
我的程式碼是參照RequireJs
編寫的這裡是官網文件 用興趣的可以直接閱讀RequireJs
的原始碼