Aitter's Blog

Requirejs源码浅析

Requirejs在项目中使用很多,平时配置好以后,只是在用,很少去探究其内部实现,只知道是通过script标签去加载依赖,监听加载成功或失败的事件,再去继续逐步加载。今天大致看了一下源码,发现其内部实现还是十分复杂的,看了一小部分,做个笔记。

AMD:Asynchronous Modules Definition异步模块定义,提供定义模块及异步加载该模块依赖的机制。

版本 RequireJS 2.3.2

文件结构

新建几个最简单的示例文件,后面的推理以这几个文件为准

主入口页

<script src="require.js"></script>
<script>
requirejs({
baseUrl:'modules/',
})
require(['main'],function(main){
main.init();
})
</script>

main.js

define(['./a'],function(a){
return {
init(){
a.init();
console.log('Main is init!')
}
}
})

a.js

define(function(){
return {
init:function(){
console.log('A is init');
}
}
})

初始化过程

从页面初始化到main模块还没有加载之前,源码里就已经执行了四次 requiresjs() 方法

req({}) => requirejs() 初始化上下文对象
req(cfg) => requirejs() 初始化默认的配置
requirejs({cusConfig}) => requirejs() 初始化用户自定义的配置
require([main]) => requirejs() 初始化入口模块

1761 req = requirejs = function (deps, callback, errback, optional) {)

从这一行可以看出 requierrequirejs 是相同的

也就是 require('a') 会走 requirejs, 同时会创建一个模块映射对象
所以上面四次 requirejs() 的执行创建了 四个模块映射对象,同时检查依赖
前面三个模块由于不是异步模块,所以不用load
到第四个模块,检测到有依赖模块 main ,于是创建了一个 main 的模块映射,并初始化了这个模块

依赖加载过程

创建 script 标签,加载 main.js 这个模块,并检查当前模块依赖是否加载完成

如果 main.js 模块已经加载完成,那么进入 define() 方法,开始解析 main.js 模块:

  1. 创建 main 模块映射
  2. 初始化 main 模块
  3. 处理 main 模块的依赖,发现有依赖 a 模块
  4. 创建 a 模块的映射
  5. 初始化 a 模块
  6. 创建 a 模块的 script 标签加载 a 模块
  7. 检查依赖是否加载完成

如果 a.js 模块加载完成,进入 define() 方法,开始解析 a.js 模块:

  1. 创建 a 模块映射
  2. 初始化 a 模块
  3. 处理依赖,发现没有依赖
  4. 检查主模块及其依赖的所有模块是否已经加载完成
  5. 所有依赖加载完成,从队列中依次执行各个模块注册的回调并传递相应的依赖


在调用的过程中,主要调用的方法就是 requirejs()define() 这两个方法,下面看一下这两个方法的内部实现。

requiresjs()

这个方法作用就是初始化上下文和加载模块

req = requirejs = function (deps, callback, errback, optional) {
//Find the right context, use default
var context, config,
contextName = defContextName;
if (!isArray(deps) && typeof deps !== 'string') {
// 如果是deps是一个配置对象,就赋值给config
config = deps;
//如果有回调 require([...], function(){})
if (isArray(callback)) {
// Adjust args if there are dependencies
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);
}
// 这里会调用 localRequire(deps, callback, errback)
// 然后调用 context.nextTick() 去初始化依赖模块并加载
return context.require(deps, callback, errback);
};

context.require()最终会执行这个方法链 nextTick() => module.init() => enable() => check() => fetch() => load()
context.nextTick()下面会说到

define()

用于定义模块,将模块推入到一个队列中,用于Module的初始化

define = function (name, deps, callback) {
var node, context;
//允许匿名模块:define([...], function(){})
if (typeof name !== 'string') {
callback = deps;
deps = name;
name = null; //没有自定义模块名称
}
//没有依赖的模块 define(function(){})
if (!isArray(deps)) {
callback = deps;
deps = null; //没有依赖
}
//没有依赖模块,按照CommonJS的模块规则,去查找依赖
if (!deps && isFunction(callback)) {
deps = [];
//如果回调没有参数,则不分析内部依赖
//如果有参数,将函数转成字符串
//用正则匹配出使用require('')方法加载的模块,将这些模块加到依赖队列中deps
if (callback.length) {
callback
//回调函数转成字符串
.toString()
//去掉所有的注释commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg,
.replace(commentRegExp, commentReplace)
//解析出require('')加载的模块,push到deps中
//cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
.replace(cjsRequireRegExp, function (match, dep) {
deps.push(dep);
});
//如果是模块commonjs的标准,那么注入 requier,exports,module 三个对象,如果不是,则注入require对象
deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
}
}
//如果IE 6-8有匿名的define,修正name和context的值
if (useInteractive) {
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute('data-requiremodule');
}
context = contexts[node.getAttribute('data-requirecontext')];
}
}
//如果有上下文,则将define的模块push到defQueue中
//如果没有,则将模块push到globalDefQueue中
if (context) {
context.defQueue.push([name, deps, callback]);
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
}
};

context.nextTick()

requirejs() 方法最终会调用这个方法,并延迟4s执行, 放在一个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;
//调用模块init方法初始化模块并加载依赖
//=>enable()注册defined和error事件
//=>check()
//=>fetch()
//=>load() //创建script标签
requireMod.init(deps, callback, errback, {
enabled: true
});
//检查是否加载完成
checkLoaded();
});

intakeDefines

对队列中的模块做初始化

function intakeDefines() {
var args;
//将globalQueue全局队列中的模块移到defQueue队列中
takeGlobalQueue();
//确保所有的依赖模块都是define()生成的模块
while (defQueue.length) {
args = defQueue.shift();//[name, deps, callback]
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 = {};
}

load

上面 从 requireMod.init() 开始,就会调用 enable()=>check()=>fetch()=>load(),到 req.load 这里,终于可以创建script标签到head中了

req.load = function (context, moduleName, url) {
var config = (context && context.config) || {}, node;
if (isBrowser) {
//创建script标签<script async type="text/javascript" charset="utf-8">
node = req.createNode(config, moduleName, url);
//设置属性
node.setAttribute('data-requirecontext', context.contextName);
node.setAttribute('data-requiremodule', moduleName);
// IE的兼容处理
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;
//Calling onNodeCreated after all properties on the node have been
//set, but before it is placed in the DOM.
if (config.onNodeCreated) {
config.onNodeCreated(node, config, moduleName, url);
}
// 将script标签添加到head中
currentlyAddingScript = node;
if (baseElement) {
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null;
return node;
}

然后调用 checkLoaded() 方法去检测是否加载成功,成功之后会触发上面注册的 context.onScriptLoad 事件,然后触发 completeLoad 事件,这里加载回来的模块也有define() 方法,那么又会调用 define() 方法注册模块,分析依赖并加依赖,依赖加载成功后,检测依赖是否还有依赖,再看全局的依赖是否全部加载完成,然后才依次触发队列里的callback回调,并将依赖模块对象传递给回调使用。

后记

花了一天的时间,终于把大致逻辑理清楚了一点,不得不说RequireJS实在太复杂了,可能是全局变量太多,依赖检查、异步加载、回调队列、事件等都有大量运用,今天也只是看了个大概,后面决定实现个简单版的requirejs再来体验一下它的精髓。

阅读参考
requirejs源码分析,使用注意要点
requireJS源码分析
细究requireJS的加载流程