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) {)

从这一行可以看出 requier 和 requirejs 是相同的

也就是 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的加载流程