这篇文章主要会讲述模块加载操做的主要流程,以及Module的主要功能。废话很少说,直接看代码吧。javascript
模块加载使用方法:css
require.config({
paths: {
jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery'
}
});
require(['jquery'], function ($) {
$(function () {
console.log('jQuery load!!!');
});
});
复制代码
咱们直接对上面的代码进行分析,假设咱们调用了require方法,须要对jquery依赖加载,require对依赖的加载,都是经过Module对象中的check方法来完成的。 在上篇中,咱们已经知道require方法只是进行了参数的修正,最后调用的方法是经过context.makeRequire方法进行构造的。 这个方法中最核心的代码在nextTick中,nextTick上篇中也分析过,nextTick方法实际上是一个定时器。html
intakeDefines();
//经过setTimeout的方式加载依赖,放入下一个队列,保证加载顺序
context.nextTick(function () {
//优先加载denfine的模块
intakeDefines();
requireMod = getModule(makeModuleMap(null, relMap));
requireMod.skipMap = options.skipMap; //配置项,是否须要跳过map配置
requireMod.init(deps, callback, errback, {
enabled: true
});
checkLoaded();
});
复制代码
咱们一步一步分析这几句代码:java
requireMod = getModule(makeModuleMap(null, relMap));
node
这里获得的实际上就是Module的实例。jquery
requireMod.init(deps, callback, errback, { enabled: true });
json
这个就是重点操做了,进行依赖项的加载。数组
先看getModle、makeModlueMap这两个方法是如何建立Module实例的。缓存
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); //获取插件
}
//Account for relative paths if there is a base name.
if (name) {
if (prefix) { //若是存在前缀
if (isNormalized) {
normalizedName = name;
} else if (pluginModule && pluginModule.normalize) {
//Plugin is loaded, use its normalize method.
normalizedName = pluginModule.normalize(name, function (name) {
return normalize(name, parentName, applyMap); //相对路径转为绝对路径
});
} else {
normalizedName = name.indexOf('!') === -1 ?
normalize(name, parentName, applyMap) :
name;
}
} else {
//一个常规模块,进行名称的标准化.
normalizedName = normalize(name, parentName, applyMap);
nameParts = splitPrefix(normalizedName); //提取插件
prefix = nameParts[0];
normalizedName = nameParts[1];
isNormalized = true;
url = context.nameToUrl(normalizedName); //将模块名转化成js的路径
}
}
suffix = prefix && !pluginModule && !isNormalized ?
'_unnormalized' + (unnormalizedCounter += 1) :
'';
return {
prefix: prefix,
name: normalizedName,
parentMap: parentModuleMap,
unnormalized: !!suffix,
url: url,
originalName: originalName,
isDefine: isDefine,
id: (prefix ?
prefix + '!' + normalizedName :
normalizedName) + suffix
};
}
//执行该方法后,获得一个对象:
{
id: "_@r2", //模块id,若是是require操做,获得一个内部构造的模块名
isDefine: false,
name: "_@r2", //模块名
originalName: null,
parentMap: undefined,
prefix: undefined, //插件前缀
unnormalized: false,
url: "./js/_@r2.js" , //模块路径
}
复制代码
这里的前缀实际上是requirejs提供的插件机制,requirejs可以使用插件,对加载的模块进行一些转换。好比加载html文件或者json文件时,能够直接转换为文本或者json对象,具体使用方法以下:app
require(["text!test.html"],function(html){
console.log(html);
});
require(["json!package.json"],function(json){
console.log(json);
});
//或者进行domReady
require(['domReady!'], function (doc) {
//This function is called once the DOM is ready,
//notice the value for 'domReady!' is the current
//document.
});
复制代码
通过makeModuleMap方法获得了一个模块映射对象,而后这个对象会被传入getModule方法,这个方法会实例化一个Module。
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 = {
//some methods
}
context = {
//some prop
Module: Module
};
复制代码
获得了Module实例以后,就是咱们的重头戏了。 能够说Module是requirejs的核心,经过Module实现了依赖的加载。
//首先调用了init方法,传入了四个参数
//分别是:依赖数组,回调函数,错误回调,配置
requireMod.init(deps, callback, errback, { enabled: true });
//咱们在看看init方法作了哪些事情
init: function (depMaps, factory, errback, options) { //模块加载时的入口
options = options || {};
if (this.inited) {
return; //若是已经被加载直接return
}
this.factory = factory;
//绑定error事件
if (errback) {
this.on('error', errback);
} else if (this.events.error) {
errback = bind(this, function (err) {
this.emit('error', err);
});
}
//将依赖数组拷贝到对象的depMaps属性中
this.depMaps = depMaps && depMaps.slice(0);
this.errback = errback;
//将该模块状态置为已初始化
this.inited = true;
this.ignore = options.ignore;
//能够在init中开启此模块为enabled模式,
//或者在以前标记为enabled模式。然而,
//在调用init以前不知道依赖关系,因此,
//以前为enabled,如今触发依赖为enabled模式
if (options.enabled || this.enabled) {
//启用这个模块和依赖。
//enable以后会调用check方法。
this.enable();
} else {
this.check();
}
}
复制代码
能够注意到,在调用init方法的时候,传入了一个option参数:
{
enabled: true
}
复制代码
这个参数的目的就是标记该模块是不是第一次初始化,而且须要加载依赖。因为enabled属性的设置,init方法会去调用enable方法。enable方法我稍微作了下简化,以下:
enable: function () {
enabledRegistry[this.map.id] = this;
this.enabled = true;
this.enabling = true;
//一、enable每个依赖, ['jQuery']
each(this.depMaps, bind(this, function (depMap, i) {
var id, mod, handler;
if (typeof depMap === 'string') {
//二、得到依赖映射
depMap = makeModuleMap(depMap,
(this.map.isDefine ? this.map : this.map.parentMap),
false,
!this.skipMap);
this.depMaps[i] = depMap; //获取的依赖映射
this.depCount += 1; //依赖项+1
//三、绑定依赖加载完毕的事件
//用来通知当前模块该依赖已经加载完毕可使用
on(depMap, 'defined', bind(this, function (depExports) {
if (this.undefed) {
return;
}
this.defineDep(i, depExports); //加载完毕的依赖模块放入depExports中,经过apply方式传入require定义的函数中
this.check();
}));
}
id = depMap.id;
mod = registry[id]; //将模块映射放入注册器中进行缓存
if (!hasProp(handlers, id) && mod && !mod.enabled) {
//四、进行依赖的加载
context.enable(depMap, this); //加载依赖
}
}));
this.enabling = false;
this.check();
},
复制代码
简单来讲这个方法一共作了三件事:
遍历了全部的依赖项
each(this.depMaps, bind(this, function (depMap, i) {}));
得到全部的依赖映射
depMap = makeModuleMap(depMap);
,这个方法前面也介绍过,用于获取依赖模块的模块名、模块路径等等。根据最开始写的代码,咱们对jQuery进行了依赖,最后获得的depMap,以下:
{
id: "jquery",
isDefine: true,
name: "jquery",
originalName: "jquery",
parentMap: undefined,
prefix:undefined,
unnormalized: false,
url: "https://cdn.bootcss.com/jquery/3.2.1/jquery.js"
}
复制代码
绑定依赖加载完毕的事件,用来通知当前模块该依赖已经加载完毕可使用
on(depMap, 'defined', bind(this, function (depExports) {});
复制代码
最后经过context.enable
方法进行依赖的加载。
context = {
enable: function (depMap) {
//在以前的enable方法中已经把依赖映射放到了registry中
var mod = getOwn(registry, depMap.id);
if (mod) {
getModule(depMap).enable();
}
}
}
复制代码
最终调用getModule方法,进行Module对象实例化,而后再次调用enable方法。这里调用的enable方法与以前容易混淆,主要区别是,以前是require模块进行enable,这里是模块的依赖进行enable操做。咱们如今再次回到那个简化后的enable方法,因为依赖的加载没有依赖项须要进行遍历,能够直接跳到enable方法最后,调用了check方法,如今咱们主要看check方法。
enable: function () {
//将当前模块id方法已经enable的注册器中缓存
enabledRegistry[this.map.id] = this;
this.enabled = true;
this.enabling = true;
//当前依赖项为空,能够直接跳过
each(this.depMaps, bind(this, function (depMap, i) {}));
this.enabling = false;
//最后调用加载器的check方法
this.check();
},
check: function () {
if (!this.enabled || this.enabling) {
return;
}
var id = this.map.id;
//一些其余变量的定义
if (!this.inited) {
// 仅仅加载未被添加到defQueueMap中的依赖
if (!hasProp(context.defQueueMap, id)) {
this.fetch(); //调用fetch() -> load() -> req.load()
}
} else if (this.error) {
//没有进入这部分逻辑,暂时跳过
} else if (!this.defining) {
//没有进入这部分逻辑,暂时跳过
}
},
复制代码
初看check方法,确实不少,足足有100行,可是不要被吓到,其实依赖加载的时候,只进了第一个if逻辑if(!this.inited)
。因为依赖加载的时候,是直接调用的加载器的enable方法,并无进行init操做,因此进入第一个if,立马调用了fetch方法。其实fetch的关键代码就一句:
Module.prototype = {
fetch: function () {
var map = this.map;
return map.prefix ? this.callPlugin() : this.load();
},
load: function () {
var url = this.map.url;
//Regular dependency.
if (!urlFetched[url]) {
urlFetched[url] = true;
context.load(this.map.id, url);
}
}
}
复制代码
若是有插件就先调用callPlugin方法,若是是依赖模块直接调用load方法。load方法先拿到模块的地址,而后调用了context.load方法。这个方法在上一章已经讲过了,大体就是动态建立了一个script标签,而后把src设置为这个url,最后将script标签insert到head标签中,完成一次模块加载。
<!--最后head标签中会有一个script标签,这就是咱们要加载的jQuery-->
<script type="text/javascript" charset="utf-8" async data-requirecontext="_" data-requiremodule="jquery" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
复制代码
到这一步,还只进行了一半,咱们只是加载jquery.js,并无拿到jquery对象。翻翻jQuery的源码,就能在最后看到jQuery使用了define进行定义。
if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}
复制代码
关于define在上一章已经讲过了,最后jQuery模块会push到globalDefQueue数组中。具体怎么从globalDefQueue中获取呢?答案是经过事件。在前面的load方法中,为script标签绑定了一个onload事件,在jquery.js加载完毕以后会触发这个事件。该事件最终调用context.completeLoad方法,这个方法会拿到全局define的模块,而后进行遍历,经过调用callGetModule,来执行define方法中传入的回调函数,获得最终的依赖模块。
//为加载jquery.js的script标签绑定load事件
node.addEventListener('load', context.onScriptLoad, false);
function getScriptData(evt) {
var node = evt.currentTarget || evt.srcElement;
removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
removeListener(node, context.onScriptError, 'error');
return {
node: node,
id: node && node.getAttribute('data-requiremodule')
};
}
context = {
onScriptLoad: function (evt) {
if (evt.type === 'load' ||
(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
interactiveScript = null;
//经过该方法能够获取当前script标签加载的js的模块名
//并移除绑定的load与error事件
var data = getScriptData(evt);
//调用completeLoad方法
context.completeLoad(data.id);
}
},
completeLoad: function (moduleName) {
var found, args, mod;
//从globalDefQueue拿到define定义的模块,放到当前上下文的defQueue中
takeGlobalQueue();
while (defQueue.length) {
args = defQueue.shift();
callGetModule(args); //运行define方法传入的回调,获得模块对象
}
//清空defQueueMap
context.defQueueMap = {};
mod = getOwn(registry, moduleName);
checkLoaded();
}
};
function callGetModule(args) {
//args内容就是define方法传入的三个参数,分别是,
//模块名、依赖数组、返回模块的回调。
//拿以前jquery中的define方法来举例,到这一步时,args以下:
//["jquery", [], function() {return $;}]
if (!hasProp(defined, args[0])) {
//跳过已经加载的模块,加载完毕后的代码都会放到defined中缓存,避免重复加载
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
复制代码
在callGetModule方法中,再次看到了getModule这个方法,这里又让咱们回到了起点,又一次构造了一个Module实例,并调用init方法。因此说嘛,Module真的是requirejs的核心。首先这个Module实例会在registry中获取,由于在以前咱们已经构造过一次了,而且直接调用了enable方法来进行js的异步加载,而后调用init方法以后的逻辑我也不啰嗦了,init会调用enable,enable又会调用check,如今咱们主要来看看check中发生了什么。
check: function () {
if (!this.enabled || this.enabling) {
return;
}
var err, cjsModule,
id = this.map.id,
depExports = this.depExports,
exports = this.exports,
factory = this.factory;
if (!this.inited) {
// 调用fetch方法,异步的进行js的加载
} else if (this.error) {
// 错误处理
this.emit('error', this.error);
} else if (!this.defining) {
this.defining = true;
if (this.depCount < 1 && !this.defined) { //若是依赖数小于1,表示依赖已经所有加载完毕
if (isFunction(factory)) { //判断factory是否为函数
exports = context.execCb(id, factory, depExports, exports);
} else {
exports = factory;
}
this.exports = exports;
if (this.map.isDefine && !this.ignore) {
defined[id] = exports; //加载的模块放入到defined数组中缓存
}
//Clean up
cleanRegistry(id);
this.defined = true;
}
this.defining = false;
if (this.defined && !this.defineEmitted) {
this.defineEmitted = true;
this.emit('defined', this.exports); //激活defined事件
this.defineEmitComplete = true;
}
}
}
复制代码
此次调用check方法会直接进入最后一个else if
中,这段逻辑中首先判断了该模块的依赖是否所有加载完毕(this.depCount < 1
),咱们这里是jquery加载完毕后来获取jquery对象,因此没有依赖项。而后判断了回调是不是一个函数,若是是函数则经过execCb方法执行回调,获得须要暴露的模块(也就是咱们的jquery对象)。另外回调也可能不是一个函数,这个与require.config中的shim有关,能够本身了解一下。拿到该模块对象以后,放到defined对象中进行缓存,以后在须要相同的依赖直接获取就能够了(defined[id] = exports;
)。
到这里的时候,依赖的加载能够说是告一段落了。可是有个问题,依赖加载完毕后,require方法传入的回调尚未被执行。那么依赖加载完毕了,我怎么才能通知以前require定义的回调来执行呢?没错,能够利用观察者模式,这里requirejs中本身定义了一套事件系统。看上面的代码就知道,将模块对象放入defined后并无结束,以后经过requirejs的事件系统激活了这个依赖模块defined事件。
激活的这个事件,是在最开始,对依赖项进行遍历的时候绑定的。
//激活defined事件
this.emit('defined', this.exports);
//遍历全部的依赖,并绑定defined事件
each(this.depMaps, bind(this, function (depMap, i) {
on(depMap, 'defined', bind(this, function (depExports) {
if (this.undefed) {
return;
}
this.defineDep(i, depExports); //将得到的依赖对象,放到指定位置
this.check();
}));
}
defineDep: function (i, depExports) {
if (!this.depMatched[i]) {
this.depMatched[i] = true;
this.depCount -= 1;
//将require对应的deps存放到数组的指定位置
this.depExports[i] = depExports;
}
}
复制代码
到这里,咱们已经有眉目了。在事件激活以后,调用defineDep方法,先让depCount减1,这就是为何check方法中须要判断depCount是否小于1的缘由(只有小于1才表示因此依赖加载完毕了),而后把每一个依赖项加载以后获得的对象,按顺序存放到depExports数组中,而这个depExports就对应require方法传入的回调中的arguments。
最后,事件函数调用check方法,咱们已经知道了check方法会使用context.execCb来执行回调。其实这个方法没什么特别,就是调用apply。
context.execCb(id, factory, depExports, exports);
execCb: function (name, callback, args, exports) {
return callback.apply(exports, args);
}
复制代码
到这里,整个一次require的过程已经所有结束了。核心仍是Module构造器,不过是require加载依赖,仍是define定义依赖,都须要经过Module,而Module中最重要的两个方法enable和check是重中之重。经过require源码的分析,对js的异步,还有早期的模块化方案有了更加深入的理解。