写在前面:
最近作的一个项目,用的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.