requirejs
作為AMD
(Asynchronous Module Definition--非同步的模組載入機制)規範的實現,還是有必要看看的。初識requirejs
原始碼,必須先弄清楚requirejs
的模組是如何定義的,並且要知道入口在哪個地方,如果清楚了呼叫方式,看原始碼的時候才會覺得順暢。
在看原始碼的過程中,我新增了一些程式碼註釋。如果要檢視新增過註釋的原始碼,可以直接在我的github上進行fork。我這裡的原始碼是目前最新的版本2.3.5。另外附上requirejs官方的原始碼。
我把requirejs一共分成了三個部分,這三個部分外面是一個閉包,並且兩個定義的全域性變數。
var requirejs, require, define;
(function (global, setTimeout) {
//1、定義一些變數與工具方法
var req, s, head ////some defined
//add some function
//2、建立一個模組載入的上下文
function newContext(contextName) {
//somecode
//定義一個模組載入器
Module = function (map) {}
Module.prototype = {
//原型鏈上
};
context = { //上下文環境
config: config, //配置
contextName: contextName, //預設為 "_"
nextTick: req.nextTick, //通過setTimeout,把執行放到下一個佇列
makeRequire: function (relMap, options) {
function localRequire () {
//somecode
//通過setTimeout的方式載入依賴,放入下一個佇列,保證載入順序
context.nextTick(function () {
intakeDefines();
requireMod = getModule(makeModuleMap(null, relMap));
requireMod.skipMap = options.skipMap;
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
return localRequire;
}
return localRequire;
}
//xxxx
}
context.require = context.makeRequire(); //載入時的入口函式
return context;
}
//3、定義require、define方法,匯入data-main路徑與進行模組載入
req = requirejs = function (deps, callback, errback, optional) {
//xxxx
context = getOwn(contexts, contextName); //獲取預設環境
if (!context) {
context = contexts[contextName] = req.s.newContext(contextName); //建立一個名為'_'的環境名
}
if (config) {
context.configure(config); //設定配置
}
return context.require(deps, callback, errback);
}
req.config = function (config) {
return req(config);
};
s = req.s = {
contexts: contexts,
newContext: newContext
};
req({}); //初始化模組載入的上下文環境
define = function (name, deps, callback) {
}
req(cfg); //載入data-main,主入口js
}(this, (typeof setTimeout === 'undefined' ? undefined : setTimeout)));
複製程式碼
上面的程式碼基本能看出requirejs
的三個部分,中間省略了很多程式碼。看過大概結構之後,來跟著我一步一步的窺探requirejs
是如何載入與定義模組的。
requirejs如何載入入口js
使用過requirejs的朋友都知道,我們會在引入requirejs的時候,在script
標籤新增data-main
屬性,作為配置和模組載入的入口。具體程式碼如下:
<script type="text/javascript" src="./require.js" data-main="./js/main.js"></script>
複製程式碼
requirejs
先通過判斷當前是否為瀏覽器環境,如果是瀏覽器環境,就遍歷當前頁面上所有的script標籤,取出其中的data-main
屬性,並通過計算,得到baseUrl和需要提前載入js的檔名。具體程式碼如下:
var isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document);
function scripts() { //獲取頁面上所有的target標籤
return document.getElementsByTagName('script');
}
function eachReverse(ary, func) {
if (ary) {
var i;
for (i = ary.length - 1; i > -1; i -= 1) {
if (ary[i] && func(ary[i], i, ary)) {
break;
}
}
}
}
if (isBrowser) {
head = s.head = document.getElementsByTagName('head')[0];
baseElement = document.getElementsByTagName('base')[0];
if (baseElement) {
head = s.head = baseElement.parentNode;
}
}
if (isBrowser && !cfg.skipDataMain) {
eachReverse(scripts(), function (script) { //遍歷所有的script標籤
//如果head標籤不存在,讓script標籤的父節點充當head
if (!head) {
head = script.parentNode;
}
dataMain = script.getAttribute('data-main');
if (dataMain) { //獲取data-main屬性(如果存在)
//儲存dataMain變數,防止轉換後任然是路徑 (i.e. contains '?')
mainScript = dataMain;
//如果沒有指定明確的baseUrl,設定data-main屬性的路徑為baseUrl
//只有當data-main的值不為一個外掛的模組ID時才這樣做
if (!cfg.baseUrl && mainScript.indexOf('!') === -1) {
//取出data-main中的路徑作為baseUrl
src = mainScript.split('/'); //通過 / 符,進行路徑切割
mainScript = src.pop(); //拿出data-main中的js名
subPath = src.length ? src.join('/') + '/' : './'; //拼接父路徑,如果data-main只有一個路徑,則表示當前目錄
cfg.baseUrl = subPath;
}
//去除js字尾,作模組名
mainScript = mainScript.replace(jsSuffixRegExp, '');
//如果mainScript依舊是一個路徑, 將mainScript重置為dataMain
if (req.jsExtRegExp.test(mainScript)) {
mainScript = dataMain;
}
//將data-main的模組名放入到deps陣列中
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
return true;
}
});
}
複製程式碼
在進行過上述操作後,我們可以得到一個cfg物件,該物件包括兩個屬性baseUrl和deps。比如我們上面的案例中,script標籤有個屬性data-main="./js/main.js"
,經過requirejs的轉後,得到的cfg物件為:
cfg = {
baseUrl: "./js/",
deps: ["main"]
}
複製程式碼
拿到cfg物件後,requirejs呼叫了req方法:req(cfg);
。req方法就是require方法,是整個requirejs的入口函式,相當於是一個分發裝置,進行引數型別的匹配,再來判斷當前是config操作還是require操作,並且在這個方法裡還會建立一個上下文環境,所有的模組載入和require相關的配置都會在這個上下文進行中進行。在呼叫req(cfg);
之前,requirejs還呼叫了一次req方法:req({});
,這一步操作就是為了建立模組載入的上下文。我們還在直接來看看req方法的原始碼吧:
//最開始定義的變數
var defContextName = '_', //預設載入的模組名
contexts = {}; //模組載入的上下文環境的容器
req = requirejs = function (deps, callback, errback, optional) {
//Find the right context, use default
var context, config,
contextName = defContextName; //預設的上下文環境
//引數修正
// Determine if have config object in the call.
if (!isArray(deps) && typeof deps !== 'string') {
// deps is a config object
config = deps; //第一個引數如果不是陣列也不是字串表示為配置引數
if (isArray(callback)) {
// 調整引數,callback此時是deps
deps = callback;
callback = errback;
errback = optional;
} else {
deps = [];
}
}
if (config && config.context) {
contextName = config.context;
}
context = getOwn(contexts, contextName); //獲取預設環境
if (!context) { //如果是第一次進入,呼叫newContext方法進行建立
context = contexts[contextName] = req.s.newContext(contextName); //建立一個名為'_'的環境名
}
if (config) {
context.configure(config); //設定配置
}
//如果只是載入配置,deps、callback、errback這幾個引數都是空,那麼呼叫require方法什麼都不會發生
return context.require(deps, callback, errback); //最後呼叫context中的require方法,進行模組載入
};
req.config = function (config) {
return req(config); //require.config方法最終也是呼叫req方法
};
if (!require) { //require方法就是req方法
require = req;
}
s = req.s = {
contexts: contexts,
newContext: newContext //建立新的上下文環境
};
複製程式碼
繼續按照之前req(cfg);
的邏輯來走,根據傳入的cfg,會呼叫context.configure(config);
,而這個context就是之前說的requirejs
三部分中的第二個部分的newContext
函式建立的,建立得到的context物件會放入全域性的contexts物件中。我們可以在控制檯列印contexts物件,看到裡面其實只有一個名為'_'
的context,這是requrejs
預設指定的上下文。
newContext函式中有許多的區域性變數用來快取一些已經載入的模組,還有一個模組載入器(Module),這個後面都會用到。還是先看呼叫的configure方法:
function newContext (contextName) {
var context, config = {};
context = {
configure: function (cfg) {
//確保baseUrl以 / 結尾
if (cfg.baseUrl) {
//所有模組的根路徑,
//預設為requirejs的檔案所在路徑,
//如果設定了data-main,則與data-main一致
if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
cfg.baseUrl += '/';
}
}
//其他程式碼,用於新增一些替他配置,與本次載入無關
//如果配置項裡指定了deps或者callback, 則呼叫require方法
//如果實在requirejs載入之前,使用require定義物件作為配置,這很有用
if (cfg.deps || cfg.callback) {
context.require(cfg.deps || [], cfg.callback);
}
},
makeRequire: function (relMap, options) {
}
}
return context;
}
複製程式碼
這個方法主要是用來做配置,在我們傳入的cfg引數中其實並不包含requirejs的主要配置項,但是在最後因為有deps屬性,邏輯能繼續往下走,呼叫了require方法:context.require(cfg.deps);
。上面的程式碼中能看出,context的require方法是使用makeRequire建立的,這裡之所以用makeRequire來建立require方法,主要使用建立一個函式作用域來儲存,方便為require方法擴充一些屬性。
context = {
makeRequire: function (relMap, options) {
options = options || {};
function localRequire(deps, callback, errback) { //真正的require方法
var id, map, requireMod;
if (options.enableBuildCallback && callback && isFunction(callback)) {
callback.__requireJsBuild = true;
}
if (typeof deps === 'string') {
//如果deps是個字串,而不是個陣列,進行一些其他處理
}
intakeDefines();
//通過setTimeout的方式載入依賴,放入下一個佇列,保證載入順序
context.nextTick(function () {
intakeDefines();
requireMod = getModule(makeModuleMap(null, relMap));
requireMod.skipMap = options.skipMap;
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
return localRequire;
}
//mixin型別與extend方法,對一個物件進行屬性擴充套件
mixin(localRequire, {
isBrowser,
isUrl,
defined,
specified
});
return localRequire;
}
};
context.require = context.makeRequire(); //載入時的入口函式
複製程式碼
最初我是使用打斷點的方式來閱讀原始碼的,每次在看到context.nextTick
的之後,就沒有往下進行了,百思不得其解。然後我看了看nextTick到底是用來幹嘛的,發現這個方法其實就是個定時器。
context = {
nextTick: req.nextTick, //通過setTimeout,把執行放到下一個佇列
};
req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
setTimeout(fn, 4);
} : function (fn) { fn(); };
複製程式碼
我也很費解,為什麼要把一些主邏輯放入到一個定時器中,這樣所有的載入都會放到下一個任務佇列進行。檢視了requirejs的版本迭代,發現nextTick是在2.10這個版本加入的,之前也沒有這個邏輯。 而且就算我把requirejs原始碼中的nextTick這段邏輯去除,程式碼也能正常執行。
tips:
這裡的setTimeout之所以設定為4ms,是因為html5規範中規定了,setTimeout的最小延遲時間(DOM_MIN_TIMEOUT_VALUE
)時,這個時間就是4ms。但是在2010年之後,所有瀏覽器的實現都遵循這個規定,2010年之前為10ms。
後來參考了網路上其他部落格的一些想法,有些人認為設定setTimeout來載入模組是為了讓模組的載入是按照順序執行的,這個目前我也沒研究透徹,先設個。todo
在這裡,哈哈哈
終於在requirejs的wiki上看到了相關文件,官方說法是為了讓模組的載入非同步化,為了防止一些細微的bug(具體是什麼bug,還不是很清楚)。
好了,還是繼續來看requirejs
的原始碼吧。在nextTick中,首先使用makeModuleMap來構造了一個模組對映,
然後立刻通過getModule新建了一個模組載入器。
//requireMod = getModule(makeModuleMap(null, relMap)); //nextTick中的程式碼
//建立模組對映
function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
var url, pluginModule, suffix, nameParts,
prefix = null,
parentName = parentModuleMap ? parentModuleMap.name : null,
originalName = name,
isDefine = true, //是否是define的模組
normalizedName = '';
//如果沒有模組名,表示是require呼叫,使用一個內部名
if (!name) {
isDefine = false;
name = '_@r' + (requireCounter += 1);
}
nameParts = splitPrefix(name);
prefix = nameParts[0];
name = nameParts[1];
if (prefix) { //如果有外掛字首
prefix = normalize(prefix, parentName, applyMap);
pluginModule = getOwn(defined, prefix); //獲取外掛
}
if (name) {
//對name再進行一些特殊處理
}
return {
prefix: prefix,
name: normalizedName,
parentMap: parentModuleMap,
unnormalized: !!suffix,
url: url,
originalName: originalName,
isDefine: isDefine,
id: (prefix ?
prefix + '!' + normalizedName :
normalizedName) + suffix
};
}
//獲取一個模組載入器
function getModule(depMap) {
var id = depMap.id,
mod = getOwn(registry, id);
if (!mod) { //對未註冊模組,新增到模組註冊器中
mod = registry[id] = new context.Module(depMap);
}
return mod;
}
//模組載入器
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;
/* this.exports this.factory
this.depMaps = [],
this.enabled, this.fetched
*/
};
Module.prototype = {
init: function () {},
fetch: function () {},
load: function () {},
callPlugin: function () {},
defineDep: function () {},
check: function () {},
enable: function () {},
on: function () {},
emit: function () {}
};
複製程式碼
requireMod.init(deps, callback, errback, {
enabled: true
});
複製程式碼
拿到建立的模組載入器之後,立即呼叫了init方法。init方法中又呼叫了enable方法,enable方法中為所有的depMap又重新建立了一個模組載入器,並呼叫了依賴項的模組載入器的enable方法,最後呼叫check方法,check方法又馬上呼叫了fetch方法,fatch最後呼叫的是load方法,load方法迅速呼叫了context.load方法。千言萬語不如畫張圖。
確實這一塊的邏輯很繞,中間每個方法都對一些作用域內的引數有一些修改,先只瞭解大致流程,後面慢慢講。 這裡重點看下req.load方法,這個方法是所有模組進行載入的方法。
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]));
}
}
};
複製程式碼
requirejs載入模組的方式是通過建立script標籤進行載入,並且將建立的script標籤插入到head中。而且還支援在webwork中使用,在webWorker使用importScripts()
來進行模組的載入。
最後可以看到head標籤中多了個script:
使用define定義一個模組
requirejs提供了模組定義的方法:define
,這個方法遵循AMD規範,其使用方式如下:
define(id?, dependencies?, factory);
複製程式碼
define三個引數的含義如下:
- id表示模組名,可以忽略,如果忽略則定義的是匿名模組;
- dependencies表示模組的依賴項,是一個陣列;
- factory表示模組定義函式,函式的return值為定義模組,如果有dependencies,該函式的引數就為這個陣列的每一項,類似於angularjs的依賴注入。
factory也支援commonjs的方式來定義模組,如果define沒有傳入依賴陣列,factory會預設傳入三個引數require, exports, module
。
沒錯,這三個引數與commonjs對應的載入方式保持一致。require用來引入模組,exports和module用來匯出模組。
//寫法1:
define(
['dep1'],
function(dep1){
var mod;
//...
return mod;
}
);
//寫法2:
define(
function (require, exports, module) {
var dep1 = require('dep1'), mod;
//...
exports = mod;
}
});
複製程式碼
廢話不多說,我們還是直接來看原始碼吧!
/**
* 用來定義模組的函式。與require方法不同,模組名必須是第一個引數且為一個字串,
* 模組定義函式(callback)必須有一個返回值,來對應第一個參數列示的模組名
*/
define = function (name, deps, callback) {
var node, context;
//執行匿名模組
if (typeof name !== 'string') {
//引數的適配
callback = deps;
deps = name;
name = null;
}
//這個模組可以沒有依賴項
if (!isArray(deps)) {
callback = deps;
deps = null;
}
//如果沒有指定名字,並且callback是一個函式,使用commonJS形式引入依賴
if (!deps && isFunction(callback)) {
deps = [];
//移除callback中的註釋,
//將callback中的require取出,把依賴項push到deps陣列中。
//只在callback傳入的引數不為空時做這些
if (callback.length) { //將模組的回撥函式轉成字串,然後進行一些處理
callback
.toString()
.replace(commentRegExp, commentReplace) //去除註釋
.replace(cjsRequireRegExp, function (match, dep) {
deps.push(dep); //匹配出所有呼叫require的模組
});
//相容CommonJS寫法
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) { //ie 6-8 進行特殊處理
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute('data-requiremodule');
}
context = contexts[node.getAttribute('data-requirecontext')];
}
}
//如果存在context將模組放到context的defQueue中,不存在contenxt,則把定義的模組放到全域性的依賴佇列中
if (context) {
context.defQueue.push([name, deps, callback]);
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
}
};
複製程式碼
通過define定義模組最後都會放入到globalDefQueue陣列中,當前上下文的defQueue陣列中。具體怎麼拿到定義的這些模組是使用takeGlobalQueue
來完成的。
/**
* 內部方法,把globalQueue的依賴取出,放到當前上下文的defQueue中
*/
function intakeDefines() { //獲取並載入define方法新增的模組
var args;
//取出所有define方法定義的模組(放在globalqueue中)
takeGlobalQueue();
//Make sure any remaining defQueue items get properly processed.
while (defQueue.length) {
args = defQueue.shift();
if (args[0] === null) {
return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' +
args[args.length - 1]));
} else {
//args are id, deps, factory. Should be normalized by the
//define() function.
callGetModule(args);
}
}
context.defQueueMap = {};
}
function takeGlobalQueue() {
//將全域性的DefQueue新增到當前上下文的DefQueue
if (globalDefQueue.length) {
each(globalDefQueue, function (queueItem) {
var id = queueItem[0];
if (typeof id === 'string') {
context.defQueueMap[id] = true;
}
defQueue.push(queueItem);
});
globalDefQueue = [];
}
}
//intakeDefines()方法是在makeRequire中呼叫的
makeRequire: function (relMap, options) { //用於構造require方法
options = options || {};
function localRequire(deps, callback, errback) { //真正的require方法
intakeDefines();
context.nextTick(function () {
//Some defines could have been added since the
//require call, collect them.
intakeDefines();
}
}
}
//同時依賴被載入完畢的時候也會呼叫takeGlobalQueue方法
//之前我們提到requirejs是向head頭中insert一個script標籤的方式載入模組的
//在載入模組的同時,為script標籤繫結了一個load事件
node.addEventListener('load', context.onScriptLoad, false);
//這個事件最後會呼叫completeLoad方法
onScriptLoad: function (evt) {
if (evt.type === 'load' ||
(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
var data = getScriptData(evt);
context.completeLoad(data.id);
}
}
completeLoad: function (moduleName) {
var found;
takeGlobalQueue();//獲取載入的js中進行define的模組
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;
}
callGetModule(args);
}
context.defQueueMap = {};
}
複製程式碼
無論是通過require的方式拿到defie定義的模組,還是在依賴載入完畢後,通過scriptLoad事件拿到定義的模組,這兩種方式最後都使用callGetModule()
這個方法進行模組載入。下面我們還是詳細看看callGetModule之後,都發生了哪些事情。
function callGetModule(args) {
//跳過已經載入的模組
if (!hasProp(defined, args[0])) {
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
複製程式碼
其實callGetModule方法就是呼叫了getModule方法(之前已經介紹過了),getModule方法返回一個Module(模組載入器)例項,最後呼叫例項的init方法。init方法會呼叫check方法,在check方法裡會執行define方法所定義的factory,最後將模組名與模組儲存到defined全域性變數中。
exports = context.execCb(id, factory, depExports, exports);
defined[id] = exports;
複製程式碼
到這裡定義模組的部分已經結束了。這篇文章先寫到這兒,這裡只理清了模組的定義和requirejs的初次載入還有requirejs的入口js是如何引入的,這一部分很多細節都沒有講到。自己挖個坑在這兒,下一部分會深入講解Module模組載入器的構成,還有require方法是如何引入依賴的。
下期再見。