從新理解前端 AMD、CMD
author: @TiffanysBearjavascript
本文主要是針對以前一些熟悉的前端概念,再次回顧的時候,結合本身的開發經驗和使用,進行再次理解。通過了開發和線上使用以後,會有更爲深入的印象。對比requirejs源碼分析,實現一個模塊加載器,須要考慮哪些問題。html
其實對於AMD和CMD的不一樣,以前一直是拘泥在使用上的不一樣。沒有深入的認識爲何會有不一樣,其實主要是由於瀏覽器端和 Node 端不一樣性能特色和瓶頸帶來的不一樣。前端
早期的js模塊化主要用於瀏覽器端,主要的需求和瓶頸在於帶寬,須要將js從服務端下載下來,從而帶來的網絡性能開銷,所以主要是知足對於做用域、按需加載的需求。所以AMD(異步模塊定義)的出現,適合瀏覽器端環境。java
然後出現Node以後,主要的性能開銷再也不是網絡性能,磁盤的讀寫和開銷能夠忽略不計;CMD在理念上更符合Node對於CommonJS的定義和理解,在須要時進行加載;可是和實際的CommonJS有區別,引入時只是產生引用指向關係。node
所以二者產生了不一樣的使用特色,在出現循環引用時,就產生了不一樣的現象。如下是針對 requirejs 源碼部分的解讀。若是有問題,歡迎提問糾正。git
一先開始是須要判斷環境,瀏覽器環境和webworker環境;若是是瀏覽器環境,經過document.createElement
建立script標籤,使用async屬性使js能進行異步加載, IE等不兼容async字段的,經過監聽 load 、 onreadystatechange 事件執行回調,監聽腳本加載完成。github
req.createNode = function (config, moduleName, url) {
var node = config.xhtml ?
document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
document.createElement('script');
node.type = config.scriptType || 'text/javascript';
node.charset = 'utf-8';
node.async = true; //建立script標籤添加了async屬性
return node;
};
req.load = function (context, moduleName, url) { //用來進行js模塊加載的方法
var config = (context && context.config) || {},
node;
if (isBrowser) { //在瀏覽器中加載js文件
node = req.createNode(config, moduleName, url); //建立一個script標籤
node.setAttribute('data-requirecontext', context.contextName); //requirecontext默認爲'_'
node.setAttribute('data-requiremodule', moduleName); //當前模塊名
if (node.attachEvent &&
!(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
!isOpera) {
useInteractive = true;
node.attachEvent('onreadystatechange', context.onScriptLoad);
} else {
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
}
node.src = url;
if (config.onNodeCreated) { //script標籤建立時的回調
config.onNodeCreated(node, config, moduleName, url);
}
currentlyAddingScript = node;
if (baseElement) { //將script標籤添加到頁面中
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null;
return node;
} else if (isWebWorker) { //在webWorker環境中
try {
setTimeout(function () { }, 0);
importScripts(url); //webWorker中使用importScripts來加載腳本
context.completeLoad(moduleName);
} catch (e) { //加載失敗
context.onError(makeError('importscripts',
'importScripts failed for ' +
moduleName + ' at ' + url,
e,
[moduleName]));
}
}
};
複製代碼
經過 setTimeout 放入下一個隊列中,保證加載順序web
//經過setTimeout的方式加載依賴,放入下一個隊列,保證加載順序
context.nextTick(function () {
//Some defines could have been added since the
//require call, collect them.
intakeDefines();
requireMod = getModule(makeModuleMap(null, relMap));
//Store if map config should be applied to this require
//call for dependencies.
requireMod.skipMap = options.skipMap;
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
複製代碼
依賴數量,是經過 depCount 來計算的,經過循環遍歷,統計具體的依賴數量;直到依賴depCount減小到0才進行下面的回調。瀏覽器
// ...
enable: function () {
enabledRegistry[this.map.id] = this;
this.enabled = true;
//Set flag mentioning that the module is enabling,
//so that immediate calls to the defined callbacks
//for dependencies do not trigger inadvertent load
//with the depCount still being zero.
this.enabling = true;
//enable每個依賴
each(this.depMaps, bind(this, function (depMap, i) {
var id, mod, handler;
if (typeof depMap === 'string') {
//Dependency needs to be converted to a depMap
//and wired up to this module.
depMap = makeModuleMap(depMap,
(this.map.isDefine ? this.map : this.map.parentMap),
false,
!this.skipMap);
this.depMaps[i] = depMap; //獲取的依賴映射
handler = getOwn(handlers, depMap.id);
if (handler) {
this.depExports[i] = handler(this);
return;
}
this.depCount += 1; //依賴項+1
on(depMap, 'defined', bind(this, function (depExports) {
if (this.undefed) {
return;
}
this.defineDep(i, depExports); //加載完畢的依賴模塊放入depExports中,經過apply方式傳入require定義的函數中
this.check();
})); //綁定defined事件,同時將dep添加到registry中
if (this.errback) {
on(depMap, 'error', bind(this, this.errback));
} else if (this.events.error) {
// No direct errback on this module, but something
// else is listening for errors, so be sure to
// propagate the error correctly.
on(depMap, 'error', bind(this, function (err) {
this.emit('error', err);
}));
}
}
id = depMap.id;
mod = registry[id];
//跳過一些特殊模塊,好比:'require', 'exports', 'module'
//Also, don't call enable if it is already enabled, //important in circular dependency cases. if (!hasProp(handlers, id) && mod && !mod.enabled) { context.enable(depMap, this); //加載依賴 } })); //Enable each plugin that is used in //a dependency eachProp(this.pluginMaps, bind(this, function (pluginMap) { var mod = getOwn(registry, pluginMap.id); if (mod && !mod.enabled) { context.enable(pluginMap, this); } })); this.enabling = false; this.check(); }, 複製代碼
判斷單個文件加載成功,是經過 checkLoaded 每間隔 50s 作一次輪詢進行判斷,變量 inCheckLoaded 做爲標識;下面是 checkLoaded 函數:緩存
function checkLoaded() {
var err, usingPathFallback,
waitInterval = config.waitSeconds * 1000,
//It is possible to disable the wait interval by using waitSeconds of 0.
expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
noLoads = [],
reqCalls = [],
stillLoading = false,
needCycleCheck = true;
//Do not bother if this call was a result of a cycle break.
if (inCheckLoaded) {
return;
}
inCheckLoaded = true;
//Figure out the state of all the modules.
eachProp(enabledRegistry, function (mod) {
var map = mod.map,
modId = map.id;
//Skip things that are not enabled or in error state.
if (!mod.enabled) {
return;
}
if (!map.isDefine) {
reqCalls.push(mod);
}
if (!mod.error) {
//If the module should be executed, and it has not
//been inited and time is up, remember it.
if (!mod.inited && expired) {
if (hasPathFallback(modId)) {
usingPathFallback = true;
stillLoading = true;
} else {
noLoads.push(modId);
removeScript(modId);
}
} else if (!mod.inited && mod.fetched && map.isDefine) {
stillLoading = true;
if (!map.prefix) {
//No reason to keep looking for unfinished
//loading. If the only stillLoading is a
//plugin resource though, keep going,
//because it may be that a plugin resource
//is waiting on a non-plugin cycle.
return (needCycleCheck = false);
}
}
}
});
if (expired && noLoads.length) {
//If wait time expired, throw error of unloaded modules.
err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
err.contextName = context.contextName;
return onError(err);
}
//Not expired, check for a cycle.
if (needCycleCheck) {
each(reqCalls, function (mod) {
breakCycle(mod, {}, {});
});
}
//If still waiting on loads, and the waiting load is something
//other than a plugin resource, or there are still outstanding
//scripts, then just try back later.
if ((!expired || usingPathFallback) && stillLoading) {
//Something is still waiting to load. Wait for it, but only
//if a timeout is not already in effect.
if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
checkLoadedTimeoutId = setTimeout(function () {
checkLoadedTimeoutId = 0;
checkLoaded();
}, 50);
}
}
inCheckLoaded = false;
}
複製代碼
這部分暫且還有點疑惑,先mark一下,以後再理解;
看到有個 breakCycle 函數,執行條件是 needCycleCheck 爲 true,可是當 !mod.inited && mod.fetched && map.isDefine
模塊未被初始化完成,可是已經獲取過定義過以後,且 在 map.prefix 有前綴,會啓動 breakCycle 檢查;至於爲何要這麼作,只能猜想是爲了到模塊require時循環引用打破輪詢查詢加載狀態等待的問題,如今先留一個疑問。
function breakCycle(mod, traced, processed) {
var id = mod.map.id;
if (mod.error) {
mod.emit('error', mod.error);
} else {
traced[id] = true;
each(mod.depMaps, function (depMap, i) {
var depId = depMap.id,
dep = getOwn(registry, depId);
//Only force things that have not completed
//being defined, so still in the registry,
//and only if it has not been matched up
//in the module already.
if (dep && !mod.depMatched[i] && !processed[depId]) {
if (getOwn(traced, depId)) {
mod.defineDep(i, defined[depId]);
mod.check(); //pass false?
} else {
breakCycle(dep, traced, processed);
}
}
});
processed[id] = true;
}
}
複製代碼
可是在CommonJs中時,存在依賴的狀況下,由於存在的只是引用,代碼執行是在實際調用時才發生,在文件的開頭和結尾也會有變量標識是否加載完成。一旦某個模塊出現循環依賴加載,就只輸出已經執行到的部分,還未執行的部分不會輸出。
因此對於AMD、CMD自己對於瀏覽器端而言,存在的只是依賴聲明的不一樣,自己各自都會先去加載依賴,CMD所謂的按需加載,其實只是寫法上的區別;本質上和AMD並沒有區別。AMD是依賴前置、CMD是依賴後置,只是在寫法上
在ES6模塊加載的循環加載狀況下,ES6是動態引用的,不存在緩存值問題,並且模塊裏面的變量綁定所在的模塊;不關心是否發生了循環加載,只是生成一個指向被加載模塊的引用,須要開發者本身來保證真正取值的時候可以取到值。