博客
博客

RequireJS 源码分析(下)

这篇文章主要会讲述模块加载操作的主要流程,以及 Module 的主要功能。废话不多说,直接看代码吧。

模块加载使用方法:


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 方法其实是一个定时器。

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();
});	

我们一步一步分析这几句代码:

  1. requireMod = getModule (makeModuleMap (null, relMap)),这里得到的实际上就是 Module 的实例。

  2. requireMod.init (deps, callback, errback, { enabled: true }),这个就是重点操作了,进行依赖项的加载。

先看 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 对象,具体使用方法如下:

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;

	//1、enable 每一个依赖, ['jQuery']
	each(this.depMaps, bind(this, function (depMap, i) {
		var id, mod, handler;

        if (typeof depMap === 'string') {
            //2、获得依赖映射 
    		depMap = makeModuleMap(depMap,
    			(this.map.isDefine ? this.map : this.map.parentMap),
    			false,
    			!this.skipMap);
    		this.depMaps[i] = depMap; // 获取的依赖映射 
    
    		this.depCount += 1; // 依赖项 + 1
    		
    		//3、绑定依赖加载完毕的事件 
    		// 用来通知当前模块该依赖已经加载完毕可以使用 
    		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) {
		    //4、进行依赖的加载 
			context.enable(depMap, this); // 加载依赖 
		}
	}));

	this.enabling = false;

	this.check();
},

简单来说这个方法一共做了三件事:

  1. 遍历了所有的依赖项

    each (this.depMaps, bind (this, function (depMap, i) {}));

  2. 获得所有的依赖映射

    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"
    }
  3. 绑定依赖加载完毕的事件,用来通知当前模块该依赖已经加载完毕可以使用

    on(depMap, 'defined', bind(this, function (depExports) {});
  1. 最后通过 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 的异步,还有早期的模块化方案有了更加深刻的理解。

支持一下
扫一扫,支持一下
  • 微信扫一扫
  • 支付宝扫一扫