本文將深度剖析require.js代碼,為了是大家更高效、正確的去使用它,本文不會介紹require的基本使用!
概要
先來一個流程圖來概要一下大概流程
在require中,根據AMD(Asynchronous Module Definition)的思想,即異步模塊加載機制,其思想就是把代碼分為一個一個的模塊來分塊加載,這樣無疑可以提高代碼的重用。
在整個require中,主要的方法就兩個:require和define,我們先來聊聊require
require
require作為主函數來引入我們的“模塊”,require會從自身的的存儲中去查找對應的defined模塊,如果沒有找到,則這時這個模塊有可以存在三種狀態:loading, enabling, defining,這里有可能就疑惑了,為什么還會有這么多狀態呢?
這就是require中要注意的地方,如果模塊還沒有被加載,那么它的這三種狀態出現的時機是:
- loading
文件還沒有加載完畢 - enabling
對該模塊的依賴進行加載和模塊化 - defining
對正在處理的模塊進行加載,並運行模塊中的callback
有同學就問了,為什么會出現這么多的狀態呢?js不是單線程操作嗎?拿過來直接加載不就完了嗎?哪來這么多事呢。而這就是require的神奇之處,我們先來瞅一瞅require的load方法的主要代碼:
req.load = function (context, moduleName, url) {
var config = (context && context.config) || {},
node;
if (isBrowser) {
//create a async script element
node = req.createNode(config, moduleName, url);
//add Events [onreadystatechange,load,error]
.....
//set url for loading
node.src = url;
//insert script element to head and start load
currentlyAddingScript = node;
if (baseElement) {
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null;
return node;
} else if (isWebWorker) {
.........
}
};
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;
return node;
};
rquire使用的是script標簽去拿js,細心的同學會注意到node上設定了async
屬性(異步加載script標簽),並且在標簽上綁定了load等事件,當文件loading完成后,則要做的主要工作是執行completeLoad
事件函數,但是要注意的是這時候把script加載完成后,立即執行的是script標簽內部的內容,執行完后才觸發的completeLoad
事件,而在我們的模塊里面,一定要用define函數來對模塊進行定義,所以這里我們先穿插着來講講define干了什么
define亂入
define顧名思義是去定義一個模塊,它只是單純的去定義嗎?錯,我不會告訴你define做了你想象不到的最神奇的事情,來瞅瞅define的代碼
define = function (name, deps, callback) {
var node,
context;
//do for multiple constructor
......
//If no name, and callback is a function, then figure out if it a
//CommonJS thing with dependencies.
if (!deps && isFunction(callback)) {
deps = [];
//Remove comments from the callback string,
//look for require calls, and pull them into the dependencies,
//but only if there are function args.
if (callback.length) {
callback
.toString()
.replace(commentRegExp, '')
.replace(cjsRequireRegExp, function (match, dep) {
deps.push(dep);
});
deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
}
}
//If in IE 6-8 and hit an anonymous define() call, do the interactive work.
if (useInteractive) {
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute('data-requiremodule');
}
context = contexts[node.getAttribute('data-requirecontext')];
}
}
//add to queue line
if (context) {
context.defQueue.push([name, deps, callback]);
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
}
};
這就是define函數,代碼不是很多,但是新奇的東西卻是有一個!!!那就是代碼中對callback.toString()
文本來進行正則匹配,哇,這是什么鬼呢?我們看看這兩個replace中的正則表達式是什么樣的
commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg;
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g;
第一個正則是用來支掉callback中的注釋的,而第二個正則是用來匹配callback.toString()
文本中的require(.....)
,並將.....
這個字段push到queue中,這個方法是不是很變態?現在讓我們來接着回到require的completeLoad
函數
require回歸
rquire的compeleteLoad函數又做了什么呢?二話不說,扔一段代碼來看看:
completeLoad : function (moduleName) {
var found,
args,
mod,
shim = getOwn(config.shim, moduleName) || {},
shExports = shim.exports;
takeGlobalQueue();
while (defQueue.length) {
args = defQueue.shift();
if (args[0] === null) {
args[0] = moduleName;
//If already found an anonymous module and bound it
//to this name, then this is some other anon module
//waiting for its completeLoad to fire.
if (found) {
break;
}
found = true;
} else if (args[0] === moduleName) {
//Found matching define call for this script!
found = true;
}
callGetModule(args);
}
context.defQueueMap = {};
//Do this after the cycle of callGetModule in case the result
//of those calls/init calls changes the registry.
mod = getOwn(registry, moduleName);
if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
if (hasPathFallback(moduleName)) {
return;
} else {
return onError(makeError('nodefine',
'No define call for ' + moduleName,
null,
[moduleName]));
}
} else {
//A script that does not call define(), so just simulate
//the call for it.
callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
}
}
checkLoaded();
}
這個函數主要是去做了從queue中拿出來define里push進去的字符串,並調用callGetModule
去調用模塊,callGetModule
又去做了什么
function callGetModule(args) {
//Skip modules already defined.
if (!hasProp(defined, args[0])) {
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
在require內部,有一個defined
全局變量來儲存已經定義好的模塊,如果這個模塊目前沒有定義,那就再做下面的makeModuleMap
,這個方法則是用來實現對當前module信息的組裝,並生成一個Map,它將會返回以下的值:
return {
prefix : prefix,
name : normalizedName,
parentMap : parentModuleMap,
unnormalized : !!suffix,
url : url,
originalName : originalName,
isDefine : isDefine,
id : (prefix ?
prefix + '!' + normalizedName :
normalizedName) + suffix
};
然后再去調用getModule
,這也是require里面來組裝module的主要方法,在require內部定義了Module類
,而這個方法則會為當前的ModuleMap
,其中包含了這個模塊的路徑等信息。這里要注意的是getModule方法里面擁有一個基於全局context的registry變量
,這里則是用來保存根據ModuleMap來實例化的Module,並將其保存在了registry
變量中(立即保存的Module只是一個空殼,后面實例中介紹),后面會介紹代碼的重用如何實現的。
我們直接來看看Module類是長什么樣的:
Module = function (map) {
this.events = getOwn(undefEvents, map.id) || {};
this.map = map;
this.shim = getOwn(config.shim, map.id);
this.depExports = [];
this.depMaps = [];
this.depMatched = [];
this.pluginMaps = {};
this.depCount = 0;
};
Module.prototype = {
//init Module
init : function (depMaps, factory, errback, options) {},
//define dependencies
defineDep : function (i, depExports) {},
//call require for plugins
fetch : function () {},
//use script to load js
load : function () {},
//Checks if the module is ready to define itself, and if so, define it.
check : function () {},
//call Plugins if them exist and defines them
callPlugin : function () {},
//enable dependencies and call defineDep
enable : function () {},
//register event
on : function (name, cb) {},
//trigger event
emit : function (name, evt) {}
}
new一個Module后,使用init來對Module對象進行初始化,並主要傳入其的依賴數組和工廠化方法。這這么多的方法里,主要的兩個方法則是enable
和check
方法,這兩個方法是對方法,當init里調用enable后,下來將要進行的就是一個不斷重復的過程,但是過程的主角在一直改變。
遞歸
上面說的這個過程那就是在初始化Model的時候去查找它的依賴,再去用load方法異步地去請求依賴,而依賴又是一個個Module,又會再對自己自身的依賴的依賴進行查找。由於這個過程都是異步進行的,所以都是通過事件監聽回調來完成調用的,我們來舉下面的例子:
如
- A 的依賴有 B C
- B 的依賴有 C D
- C 的依賴有 A B
這是一個很繞的例子,如A,B,C都有自己的方法,而我們在實現時都互相調用了各自的方法,我們姑且不討論這種情況的現實性。
當如果我去require("A")
時,require去查找defined
中是否有A模塊,如果沒有,則去調用makeModuleMap
來為即將調用的模塊實例一個ModuleMap
並加入到defined中,再用ModuleMap實例化一個Module
加入到registry中,但是這時候的Module是一個空殼,它是只存儲了一些模塊相關的依賴等,模塊里的exports或者callback是還沒有被嵌進來,因為這個文件根本沒有被加載呀!
注冊時觸發Module.init
方法去異步加載文件(使用script)。加載完畢后,觸發A里的define函數,define函數通過參數或callback里查找A模塊需要的依賴,即B和C模塊,將B,C加入到A的依賴數組中。這時則觸發completeLoad
函數,這時complete再去從queue中遍歷,調用callGetModule
去查找B、C模塊,這時則會創建B和C模塊的ModuleMap,根據ModuleMap去實例化空殼Module,(調用異步load加載,再觸發define等,繼續查找依賴…………),再接下來會做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 ((!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;
}
這個函數對所有已在registry
中的Module進行遍歷,並來判斷其是否已經完成了定義(定義是在Module.check
函數里完成的,定義完成后ModuleMap.isDefined = true
,並將其從registry中刪除,其會將真正的模塊內容注入到對應的defined中),注意這里有一個重要的地方,checkoutLoadTimeoutId
是一個間隔為50ms的setTimeout函數,即當在加載的時候會不斷輪詢去查看所有模塊是否已經加載好了,因為所有的模塊都是異步進行加載的,所以這樣可以完全保證所有模塊進行完全加載,並進行了過期設定。
接着上面的例子講,當加載B模塊時,會去查找A和C模塊,這時候A模塊是已經加載的,但是不能確定C是否已經加載好,但是這時的C模塊空殼Module已經加入到了registry中,所以這時會像上面去輪詢C模塊是否加載,C模塊不加載好,是無法對B模塊進行注入的,B模塊在這一階段仍是那一個registry里的空殼Module,直至C模塊已經定義,B模塊的depCount成為0,才可以繼續運行去注入自己。在對模塊進行define的時候,用上了defining,是為了防止內部的factory進行加工時,再去嘗試去define這個Module,就像一個圈一樣,掐斷了它。
這就是整個require工作的流程,其中主要使用了異步加載,所以讓這個思想變得異常的復雜,但是帶來的卻是性能上的優化,需要我們注意的是:
在使用require時,我們需要注意依賴包的引入,如果我們把B的改成define("B",[],callback)
,這時B是沒有callback依賴預讀,那么我們在引入A模塊的時候異步加載了B和C模塊,但是B模塊里使用了C模塊的方法,這里的B是直接運行的,並不去檢測其的依賴包是否加載完畢,所以這時的B運行時碰到require("C")
時,C模塊是否加載好是不確定的,這時候代碼會不會出問題就是網速的問題了……………………
小心
我們在使用時要小心define()的用法:
- define(name, dependencies, callback)
將依賴寫在參數dependencies中,這樣require時會對里面的依賴進行加載,加載完后才會執行callback - define(dependencies, callback)
同上 - define(name, callback)
直接在callback中require依賴,會對callback.toString()
進行正則查找require(....)
,同樣加載查找出的所有依賴並加載完后執行callback - define(callback)
同上
使用時千萬不能在第一、二種情況下直接require依賴,這樣並不能保障該模塊是否已被定義下執行了callback。
在使用后兩種情況時,必須要注意的一點是,在require中對callback使用了callback.length
判斷,如果回調中沒有加參數,則不會進行callback.toString()
進行查找,所以我們在這兩種情況使用時務必要加上參數,哪怕是沒用的,如define(function(require){})
。
可以看出,require總是在自動的分析對模塊的依賴並去加載它們,如果我們一次性在文件中都互相引入了所有模塊,那就會使得整個模塊化過程變得累贅不堪,就是說即使當前只需要兩三個模塊來完成頁面,而其它很十幾二十個模塊都會被加載進來,雖然模塊化達成了,但是效率卻是下降了。所以在使用的使用我們要去智能地判斷目前立即需要的模塊,而非立即的模塊我們可以在空閑時間進行加載,而並是不去影響主頁面的加載和渲染,這在實現單頁面應用時及其重要。
Finish.