這篇文章主要會講述模塊加載操做的主要流程,以及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的異步,還有早期的模塊化方案有了更加深入的理解。