寫在前面:
最近作的一個項目,用的require和backbone,對二者的使用已經很熟悉了,可是一直都有好奇他們怎麼實現的,一直尋思着讀讀源碼。如今項目結束,終於有機會好好研究一下。javascript
本文重要解讀requirejs的源碼,backbone源碼分析將會隨後給出。html
行文思路:java
- requirejs 基本介紹
- requirejs使用後的幾個好奇
- requirejs源碼解讀
requirejs基本介紹
因爲篇幅有限,這裏不會詳解requirejs的使用和api,建議讀者朋友本身去用幾回,再詳讀api.node
簡介
簡單來講,requirejs是一個遵循 AMD(Asynchronous Module Definition)規範的模塊載入框架。jquery
使用requirejs的好處是:異步加載、模塊化、依賴管理git
使用
引入:github
<script data-main="/path/to/init" src="/path/to/require.js"></script>web
這裏的data-main是整個項目的入口.api
定義模塊:數組
define(
'
jquery
', function($) {
return {
hello: function() {
console.log(
'
hello
');
}
};
});
配置:
require.config({
baseUrl:
"
/client/src/script/js/
" ,
paths: {
jquery:
"
third_party_lib/jquery
"
} ,
shim: {
jquery: {
exports:
"
$
"
}
},
waitSeconds: 1s,
});
非AMD規範插件使用(不少插件和requirejs結合不能使用的解決方案)
A: AMD化. 在插件最外層包一層 define({});
B: config.shim 的exports選項. 做用是把全局變量引入.
requirejs使用後的幾個好奇
- 使用require後頁面引入的js兩個data-*屬性,有什麼用?
-
- data-main 是項目的惟一入口,可是怎麼進入這個入口,以後又作了什麼?
- require和define裏面怎麼執行的,如何管理依賴關係?
- require和define區別是什麼?
- 如何異步加載js文件的?
requirejs源碼解讀
本文的 require.js version = 「2.1.11」
requirejs源碼的組織:
如註釋,整個源碼我分紅了三個部分。
var requirejs, require, define;
(function (
global) {
var req, s, head, baseElement, dataMain, src,..;
function isFunction(it) {}
function isArray(it) {}
function each(ary, func) {}
function eachReverse(ary, func) {}
function hasProp(obj, prop) {}
function getOwn(obj, prop) {}
function eachProp(obj, func) {}
function mixin(target, source, force, deepStringMixin) {} ...;
//
第一部分結束
function newContext(contextName) {
var inCheckLoaded, Module, context, handlers,
function trimDots(ary) {}
function normalize(name, baseName, applyMap) {}
function removeScript(name) {}
function hasPathFallback(id) {}
function splitPrefix(name) {}
function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {}
function getModule(depMap) {}
function on(depMap, name, fn) {}
function onError(err, errback) {}
function takeGlobalQueue() {}
handlers = {};
function cleanRegistry(id) {}
function breakCycle(mod, traced, processed) {}
function checkLoaded() {}
Module = function (map) {};
Module.prototype = {};
function callGetModule(args) {}
function removeListener(node, func, name, ieName) {}
function getScriptData(evt) {}
function intakeDefines() {}
context = {}
context.require = context.makeRequire();
return context;
}
//
第二部分結束
req = requirejs = function (deps, callback, errback, optional) {};
req.config = function (config) {};
req.nextTick =
typeof setTimeout !==
'
undefined
' ? function (fn) {
setTimeout(fn,
4);
} : function (fn) { fn(); };
req.onError = defaultOnError;
req.createNode = function (config, moduleName, url) {};
req.load = function (context, moduleName, url) {};
define = function (name, deps, callback) {};
req.exec = function (text) {};
req(cfg);
//
第三部分結束
}(
this
));
第一部分是定義一些全局變量和helper function. 第二部分和第三部分會頻繁用到。
第二部分是定義newContext的一個func, 項目的核心邏輯。
第三部分是定義require和define兩個func以及項目入口。
詳細分析:
data-main入口實現:
//
項目入口, 尋找有data-main的script.
if (isBrowser && !cfg.skipDataMain) {
//
scripts()返回頁面全部的script. 逆向遍歷這些script直到有一個func返回true
eachReverse(scripts(), function (script) {
if (!head) {
head = script.parentNode;
}
dataMain = script.getAttribute(
'
data-main
');
if (dataMain) {
mainScript = dataMain;
//
若是config沒有配置baseUrl, 則含有data-main 指定文件所在的目錄爲baseUrl.
if (!cfg.baseUrl) {
//
data-main 指向'./path/to/a', 則mainScript爲a.js, baseUrl 爲./path/to
src = mainScript.split(
'
/
');
mainScript = src.pop();
subPath = src.length ? src.join(
'
/
') +
'
/
' :
'
./
';
cfg.baseUrl = subPath;
}
//
若是mainScript包括.js則去掉,讓他表現的像一個module name
mainScript = mainScript.replace(jsSuffixRegExp,
'');
if (req.jsExtRegExp.test(mainScript)) {
mainScript = dataMain;
}
//
把data-main指向的script放入cfg.deps中, 做爲第一個load
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
return
true;
}
});
}
define實現:
define = function (name, deps, callback) {
var node, context;
//
匿名模塊
if (
typeof name !==
'
string
') {
//
第一個參數調整爲deps, 第二個callback
callback = deps;
deps = name;
name =
null;
}
//
沒有deps
if (!isArray(deps)) {
callback = deps;
deps =
null;
}
if (!deps && isFunction(callback)) {
deps = [];
//
.toString 把callback變成字符串方便調用字符串的func.
//
.replace 把全部註釋去掉. commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
//
.replace 把callback裏面的 require所有push 到 deps
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);
}
}
//
若是IE6-8有匿名define, 修正name和context的值
if (useInteractive) {
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute(
'
data-requiremodule
');
}
context = contexts[node.getAttribute(
'
data-requirecontext
')];
}
}
//
若是當前context存在, 把本次define加入到defQueue中
//
不然加入globalDefQueue
(context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
};
加載js文件:
req.load = function (context, moduleName, url) {
var config = (context && context.config) || {},
node;
if (isBrowser) {
node = req.createNode(config, moduleName, url);
//
因此每一個經過requirej引入的js文件有這兩個屬性,用來移除匹配用的.或則IE低版本中修正contextName和moduleName
node.setAttribute(
'
data-requirecontext
', context.contextName);
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;
//
兼容IE6-8, script可能在append以前執行, 全部把noe綁定在currentAddingScript中,防止其餘地方改變這個值
currentlyAddingScript = node;
if (baseElement) {
//
這裏baseElement是getElementsByName('base'); 如今通常都執行else了。
head.insertBefore(node, baseElement);
}
else {
head.appendChild(node);
}
currentlyAddingScript =
null;
return node;
}
else
if (isWebWorker) {
//
若是是web worker。。不懂
try {
importScripts(url);
context.completeLoad(moduleName);
}
catch (e) {
context.onError(makeError(
'
importscripts
',
'
importScripts failed for
' +
moduleName +
'
at
' + url,
e,
[moduleName]));
}
}
};
這裏能夠看出第一個問題的緣由了.引入data-*的做用是用來移除匹配用的.或則IE低版本中修正contextName和moduleName. 這裏req.createNode和context.onScriptLoad是其餘地方定義的,接下來看req.createNope:
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;
};
這裏能夠解決最後一個問題,經過appendChild, node.async實現異步加載的。
當node加載完畢後會調用context.onScriptLoad, 看看作了什麼:
onScriptLoad:
function (evt) {
if (evt.type === 'load' ||
(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
interactiveScript =
null;
//
getScriptData()找evet對應的script, 提取data-requiremodule就知道mod的name了。
var data = getScriptData(evt);
context.completeLoad(data.id);
}
}
再看context.completeLoad:
completeLoad:
function (moduleName) {
var found, args, mod,
shim = getOwn(config.shim, moduleName) || {},
shExports = shim.exports;
//
把globalQueue 轉換到 context.defQueue(define收集到的mod集合)
takeGlobalQueue();
while (defQueue.length) {
args = defQueue.shift();
if (args[0] ===
null) {
args[0] = moduleName;
//
若是當前的defModule是匿名define的(arg[0]=null), 把當前moduleName給他,並標記找到
if (found) {
break;
}
found =
true;
}
else
if (args[0] === moduleName) {
//
非匿名define
found =
true;
}
//
callGetModule較長, 做用是實例化一個context.Module對象並初始化, 放入registry數組中表示可用.
callGetModule(args);
}
//
獲取剛纔實例化的Module對象
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 {
callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
}
}
//
檢查loaded狀況,超過期間的就remove掉,並加入noLoads數組
checkLoaded();
}
能夠看到,當script加載完畢後,只作了一件事:實例化context.Module對象,並暴露給registry供調用.
require 實現
req = requirejs =
function (deps, callback, errback, optional) {
var context, config,
contextName = defContextName;
//
第一個參數不是模塊依賴表達
if (!isArray(deps) &&
typeof deps !== 'string') {
//
deps is a config object
config = deps;
if (isArray(callback)) {
//
若是有依賴模塊則調整參數, 第二個參數是deps
deps = callback;
callback = errback;
errback = optional;
}
else {
deps = [];
}
}
if (config && config.context) {
contextName = config.context;
}
context = getOwn(contexts, contextName);
if (!context) {
context = contexts[contextName] = req.s.newContext(contextName);
}
if (config) {
context.configure(config);
}
//
以上獲取正確的context,contextName
return context.require(deps, callback, errback);
};
一看,結果什麼都沒作,作的事還在context.require()裏面。 在context對象中:
context.require = context.makeRequire();
咱們須要的require結果是context.makeRequire這個函數返回的閉包:
makeRequire:
function (relMap, options) {
options = options || {};
function localRequire(deps, callback, errback) {
var id, map, requireMod;
if (options.enableBuildCallback && callback && isFunction(callback)) {
callback.__requireJsBuild =
true;
}
if (
typeof deps === 'string') {
if (isFunction(callback)) {
//
非法調用
return onError(makeError('requireargs', 'Invalid require call'), errback);
}
//
若是require的是require|exports|module 則直接調用handlers定義的
if (relMap && hasProp(handlers, deps)) {
return handlers[deps](registry[relMap.id]);
}
if (req.get) {
return req.get(context, deps, relMap, localRequire);
}
//
經過require的模塊名Normalize成須要的moduleMap對象
map = makeModuleMap(deps, relMap,
false,
true);
id = map.id;
if (!hasProp(defined, id)) {
return onError(makeError('notloaded', 'Module name "' +
id +
'" has not been loaded yet for context: ' +
contextName +
(relMap ? '' : '. Use require([])')));
}
//
返回require的模塊的返回值。
return defined[id];
}
//
把globalQueue 轉換到 context.defQueue,並把defQueue的每個都實例化一個context.Module對象並初始化, 放入registry數組中表示可用.
intakeDefines();
//
nextTick 使用的是setTimeOut.若是沒有則是回調函數
//
本次require結束後把全部deps標記爲needing to be loaded.
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;
}
若是直接require模塊, 會返回此模塊的返回值;不然會把他加入到context.defQueue, 初始化後等待調用; 好比直接:
var util = require('./path/to/util');
會直接返回util模塊返回值; 而若是:
require(['jquery', 'backbone'], function($, Backbone){});
就會執行intakeDefines()
和nextTick()
;
總結
花時間讀讀源碼,對之前使用require
時的那些作法想通了,知道那樣作的緣由和結果,之後用着確定也會順手多了。
學習框架源碼可讓本身用的有譜、大膽, 更多的時候是學習高手的代碼組織, 編碼風格,設計思想, 對本身提高幫助很大~
總結下本身研讀源碼方式,但願對讀者有所幫助: 項目中用熟 -> 源碼如何佈局組織-> demo進入源碼,log驗證猜測-> 看別人分析 -> 總結
玉伯說 RequireJS
是沒有明顯的 bug,SeaJS
是明顯沒有 bug, 之後必定研究下seajs
,看看如何明顯沒有bug.