RequireJS源碼分析(下)

這篇文章主要會講述模塊加載操做的主要流程,以及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

  1. requireMod = getModule(makeModuleMap(null, relMap));node

    這裏獲得的實際上就是Module的實例。jquery

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

複製代碼

簡單來講這個方法一共作了三件事:

  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) {});
    複製代碼
  4. 最後經過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的異步,還有早期的模塊化方案有了更加深入的理解。

相關文章
相關標籤/搜索