【requireJS原始碼學習01】瞭解整個requireJS的結構
前言
現在工作中基本離不開requireJS這種模組管理工具了,之前一直在用,但是對其原理不甚熟悉,整兩天我們來試著學習其原始碼,而後在探尋其背後的AMD思想吧
於是今天的目標是熟悉requireJS整體框架結構,順便看看之前的簡單demo
程式入口
原始碼閱讀仍然有一定門檻,通看的做法不適合我等素質的選手,所以還是得由入口開始,requireJS的入口便是引入時候指定的data-main
<script src="require.js" type="text/javascript" data-main="main.js"></script>
在js引入後,會自動執行指向data-main的js函式,這個就是我們所謂的入口,跟著這條線,我們就進入了requirejs的大門
首先,引入js檔案本身不會幹什麼事情,那麼requirejs內部做了什麼呢?
① 除了一些初始化操作以為第一件乾的事情,值執行這段程式碼:
//Create default context. req({});
這段程式碼會構造預設的引數,其呼叫的又是整個程式的入口
req = requirejs = function (deps, callback, errback, optional) {}
這裡具體幹了什麼我們先不予關注,繼續往後面走,因為貌似,這裡與data-main暫時不相干,因為這段會先於data-main邏輯執行
然後,進入data-main相關的邏輯了:
//Look for a data-main script attribute, which could also adjust the baseUrl. if (isBrowser && !cfg.skipDataMain) { //Figure out baseUrl. Get it from the script tag with require.js in it. eachReverse(scripts(), function (script) { //Set the 'head' where we can append children by //using the script's parent. if (!head) { head = script.parentNode; } //Look for a data-main attribute to set main script for the page //to load. If it is there, the path to data main becomes the //baseUrl, if it is not already set. dataMain = script.getAttribute('data-main'); if (dataMain) { //Preserve dataMain in case it is a path (i.e. contains '?') mainScript = dataMain; //Set final baseUrl if there is not already an explicit one. if (!cfg.baseUrl) { //Pull off the directory of data-main for use as the //baseUrl. src = mainScript.split('/'); mainScript = src.pop(); subPath = src.length ? src.join('/') + '/' : './'; cfg.baseUrl = subPath; } //Strip off any trailing .js since mainScript is now //like a module name. mainScript = mainScript.replace(jsSuffixRegExp, ''); //If mainScript is still a path, fall back to dataMain if (req.jsExtRegExp.test(mainScript)) { mainScript = dataMain; } //Put the data-main script in the files to load. cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; return true; } }); }
因為requireJS不止用於瀏覽器,所以這裡有一個判斷,我們暫時不予關注,看看他幹了些什麼
① 他會去除頁面所有的script標籤,然後倒敘遍歷之
scripts() => [<script src="require.js" type="text/javascript" data-main="main.js"></script>]
這個地方遇到兩個方法
eachReverse
與each一致,只不過由逆序遍歷
function eachReverse(ary, func) { if (ary) { var i; for (i = ary.length - 1; i > -1; i -= 1) { if (ary[i] && func(ary[i], i, ary)) { break; } } } }View Code
scripts
便是document.getElementsByTagName('script');返回所有的script標籤
然後開始的head便是html中的head標籤,暫時不予理睬
if (isBrowser) { head = s.head = document.getElementsByTagName('head')[0]; //If BASE tag is in play, using appendChild is a problem for IE6. //When that browser dies, this can be removed. Details in this jQuery bug: //http://dev.jquery.com/ticket/2709 baseElement = document.getElementsByTagName('base')[0]; if (baseElement) { head = s.head = baseElement.parentNode; } }View Code
dataMain = script.getAttribute('data-main');
然後這一句便可以獲取當前指定執行的檔名,比如這裡
dataMain => main.js
如果不存在就不會有什麼操作了
PS:我原來記得預設指向main.js,看來是我記錯了......
然後下來做了一些處理,會根據指定的main.js初步確定bashUrl,其實就是與main.js統一目錄
最後做了關鍵的一個步驟:
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
將main放入帶載入的配置中,而本身不幹任何事情,繼續接下來的邏輯......然後此邏輯暫時結束,根據這些引數進入下一步驟
req/requirejs
根據上一步驟的處理,會形成上面截圖的引數,而後再一次執行入口函式req,這個時候就會發生不一樣的事情了
/** * Main entry point. * * If the only argument to require is a string, then the module that * is represented by that string is fetched for the appropriate context. * * If the first argument is an array, then it will be treated as an array * of dependency string names to fetch. An optional function callback can * be specified to execute when all of those dependencies are available. * * Make a local req variable to help Caja compliance (it assumes things * on a require that are not standardized), and to give a short * name for minification/local scope use. */ req = requirejs = function (deps, callback, errback, optional) { //Find the right context, use default var context, config, contextName = defContextName; // Determine if have config object in the call. if (!isArray(deps) && typeof deps !== 'string') { // deps is a config object config = deps; 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); } return context.require(deps, callback, errback); };
這個時候我們的第一個引數deps就不再是undefined了,而是一個物件,這裡便將其配置放到了config變數中保持deps為一陣列,然後幹了些其他事情
這裡有個變數context,需要特別注意,後面我們來看看他有些什麼,這裡有一個新的函式
function getOwn(obj, prop) { return hasProp(obj, prop) && obj[prop]; } function hasProp(obj, prop) { return hasOwn.call(obj, prop); } hasOwn = op.hasOwnProperty
這裡會獲取非原型屬性將其擴充套件,首次執行時候會碰到一個非常重要的函式newContext 因為他是一個核心,我們這裡暫時選擇忽略,不然整個全部就陷進去了
經過newContext處理後的context就變成這個樣子了:
if (config) { context.configure(config); }
這裡就會將我們第一步的引數賦值進物件,具體幹了什麼,我們依舊不予理睬,main.js幹了兩件事情:
① 暫時性設定了baseUrl
② 告訴requireJS你馬上要載入我了
於是最後終於呼叫require開始處理邏輯
return context.require(deps, callback, errback);
require
因為context.require = context.makeRequire();而該函式本身又返回localRequire函式,所以事實上這裡是執行的localRequire函式,內部維護著一個閉包
因為nextContext只會執行一次,所以很多require實際用到的變數都是nextContext閉包所維護,比如我們這裡便可以使用config變數
這裡依舊有一些特殊處理,比如deps是字串的情況,但是我們暫時不予關注.......
PS:搞了這麼久很多不予關注了,欠了很多帳啊!
他這裡應該是有一個BUG,所以這裡用到了一個settimeout延時
PS:因為settimeout的使用,整個這塊的程式全部會拋到主幹邏輯之後了
然後接下來的步驟比較關鍵了,我們先拋開一切來理一理這個newContext
newContext
newContext佔了原始碼的主要篇幅,他也只會在初始化時候執行一次,而後便不再執行了:
if (!context) { context = contexts[contextName] = req.s.newContext(contextName); }
現在,我們就目前而知來簡單理一理,requireJS的結構
① 變數宣告,工具類
在newContext之前,完全是做一些變數的定義,或者做一些簡單的操作,裡面比較關鍵的是contexts/cfg物件,會被後面無數次的用到
② 例項化上下文/newContext
緊接著就是newContext這洋洋灑灑一千多行程式碼了,其中主要乾了什麼暫時不知道,據我觀察應該是做環境相關的準備
③ 對外介面
上面操作結束後便提供了幾個主要對外介面
requirejs
require.config
雖然這裡是兩個函式,其實都是requirejs這一關入口
而後,require自己擼了一把,例項化了預設的引數,這裡便呼叫了newContext,所以以後都不會呼叫,其中的函式多處於其閉包環境
接下來根據引入script標籤的data-main做了一次文章,初始化了簡單的引數,並將main.js作為了依賴項,這裡會根據main.js重寫cfg物件
最後requirejs執行一次reg(cfg),便真的開始了所有操作,這個時候我們就進入newContext,看看他主要乾了什麼
PS:所有require並未提供任何藉口出來,所以在全域性想檢視其contexts或者cfg是不行的,而且每次操作都可能導致其改變
要了解newContext函式,還是需要進入其入口
if (!context) { context = contexts[contextName] = req.s.newContext(contextName); }
從script標籤引入require庫時候,會因為這段程式碼執行一次newContext函式,從此後,該函式不會被執行,其實現的原因不是我等現在能明白的,先看懂實現再說吧
//Create default context. req({});
所以上面說了那麼多,看了這麼久,其實最關鍵的還是首次載入,首次載入就決定了執行上下文了
整體結構
newContext的基本結構大概是這樣:
① 函式作用域內變數定義(中間初始化了一發handlers變數)
② 一堆工具函式定義
③ Module模組(這塊給人的感覺不明覺厲...應該是核心吧)
④ 例項化context物件,將該物件返回,然後基本結束
進入newContext後,第一步是基本變數定義,這種對外的框架一般都不會到處命名變數,而是將所有變數全部提到函式最前面
一來是js解析時候宣告本身會提前,而來可能是到處命名變數會讓我們找不到吧......
開始定義了很多變數,我們一來都不知道是幹神馬的,但是config變數卻引起了我們的注意,這裡先放出來,繼續往下就是一連串的函數了,值得說明的是,這些變數會被重複利用哦
一眼看下來,該函式本身並沒有做什麼實際的事情,這個時候我們就需要找其入口,這裡的入口是
//首次呼叫 req({}) => //觸發newContext,做首次初始化並返回給context物件 context = contexts[contextName] = req.s.newContext(contextName) => //注意這裡require函式其實處於了mackRequire函式的閉包環境 context.require = context.makeRequire(); => //首次呼叫newContext返回物件初始化變數 context.configure(config);
所以,在首次初始化後,並未做特別的處理,直到configure的呼叫,於是讓我們進入該函式
/** * Set a configuration for the context. * @param {Object} cfg config object to integrate. */ configure: function (cfg) { //Make sure the baseUrl ends in a slash. if (cfg.baseUrl) { if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { cfg.baseUrl += '/'; } } //Save off the paths and packages since they require special processing, //they are additive. var pkgs = config.pkgs, shim = config.shim, objs = { paths: true, config: true, map: true }; eachProp(cfg, function (value, prop) { if (objs[prop]) { if (prop === 'map') { if (!config.map) { config.map = {}; } mixin(config[prop], value, true, true); } else { mixin(config[prop], value, true); } } else { config[prop] = value; } }); //Merge shim if (cfg.shim) { eachProp(cfg.shim, function (value, id) { //Normalize the structure if (isArray(value)) { value = { deps: value }; } if ((value.exports || value.init) && !value.exportsFn) { value.exportsFn = context.makeShimExports(value); } shim[id] = value; }); config.shim = shim; } //Adjust packages if necessary. if (cfg.packages) { each(cfg.packages, function (pkgObj) { var location; pkgObj = typeof pkgObj === 'string' ? { name: pkgObj} : pkgObj; location = pkgObj.location; //Create a brand new object on pkgs, since currentPackages can //be passed in again, and config.pkgs is the internal transformed //state for all package configs. pkgs[pkgObj.name] = { name: pkgObj.name, location: location || pkgObj.name, //Remove leading dot in main, so main paths are normalized, //and remove any trailing .js, since different package //envs have different conventions: some use a module name, //some use a file name. main: (pkgObj.main || 'main') .replace(currDirRegExp, '') .replace(jsSuffixRegExp, '') }; }); //Done with modifications, assing packages back to context config config.pkgs = pkgs; } //If there are any "waiting to execute" modules in the registry, //update the maps for them, since their info, like URLs to load, //may have changed. eachProp(registry, function (mod, id) { //If module already has init called, since it is too //late to modify them, and ignore unnormalized ones //since they are transient. if (!mod.inited && !mod.map.unnormalized) { mod.map = makeModuleMap(id); } }); //If a deps array or a config callback is specified, then call //require with those args. This is useful when require is defined as a //config object before require.js is loaded. if (cfg.deps || cfg.callback) { context.require(cfg.deps || [], cfg.callback); } },View Code
首次傳入的是空物件,所以開始一段程式碼暫時沒有意義,這裡使用的config變數正是newContext維護的閉包,也就是上面讓注意的
config = { //Defaults. Do not set a default for map //config to speed up normalize(), which //will run faster if there is no default. waitSeconds: 7, baseUrl: './', paths: {}, pkgs: {}, shim: {}, config: {} },
下面用到了一個新的函式:
eachProp
這個函式會遍歷物件所有非原型屬性,並且使用第二個引數(函式)執行之,如果返回true便停止,首次執行時候cfg為空物件,便沒有往下走,否則config變數會被操作,具體我們暫時不管
/** * Cycles over properties in an object and calls a function for each * property value. If the function returns a truthy value, then the * iteration is stopped. */ function eachProp(obj, func) { var prop; for (prop in obj) { if (hasProp(obj, prop)) { if (func(obj[prop], prop)) { break; } } } }View Code
這個所謂的入口執行後實際的意義基本等於什麼都沒有幹......
但是,這裡可以得出一個弱弱的結論就是
configure是用於設定引數滴
所以所謂的入口其實沒有幹事情,這個時候第二個入口便出現了
context.require
return context.require(deps, callback, errback);
引數設定結束後便會執行context的require方法,這個是真正的入口,他實際呼叫順序為:
context.require = context.makeRequire(); => localRequire
所以真正呼叫localRequire時候,已經執行了一番makeRequire函數了,現在處於了其上下文,正因為localRequire被處理過,其多了幾個函式屬性
除此之外,暫時沒有看出其它變化,所以這裡在某些特定場景是等價的
function localRequire(deps, callback, errback) { var id, map, requireMod; if (options.enableBuildCallback && callback && isFunction(callback)) { callback.__requireJsBuild = true; } if (typeof deps === 'string') { if (isFunction(callback)) { //Invalid call return onError(makeError('requireargs', 'Invalid require call'), errback); } //If require|exports|module are requested, get the //value for them from the special handlers. Caveat: //this only works while module is being defined. if (relMap && hasProp(handlers, deps)) { return handlers[deps](registry[relMap.id]); } //Synchronous access to one module. If require.get is //available (as in the Node adapter), prefer that. if (req.get) { return req.get(context, deps, relMap, localRequire); } //Normalize module name, if it contains . or .. map = makeModuleMap(deps, relMap, false, true); id = map.id; if (!hasProp(defined, id)) { return onError(makeError('notloaded', 'Module name "' + id + '" has not been loaded yet for context: ' + contextName + (relMap ? '' : '. Use require([])'))); } return defined[id]; } //Grab defines waiting in the global queue. intakeDefines(); //Mark all the dependencies as needing to be loaded. 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; requireMod.init(deps, callback, errback, { enabled: true }); checkLoaded(); }); return localRequire; }View Code
過程中會執行一次intakeDefines,他的意義是定義全域性佇列,其意義暫時不明,然後進入了前面說的那個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; requireMod.init(deps, callback, errback, { enabled: true }); checkLoaded(); });
這段程式碼事實上是比較奇特的,他會完全脫離整個require程式碼,比如整個
return context.require(deps, callback, errback);
執行了後上面才會慢慢執行
PS:require這段比較重要,留待明天分析,今天先看整體邏輯
下面的主要邏輯又到了這裡
requireMod = getModule(makeModuleMap(null, relMap));
我們這裡主要先看getModule先,首先makeModuleMap比較關鍵,他會根據規則建立一些模組唯一標識的東西,暫時是什麼當然是先不管啦......
PS:其規則應該與載入的require數量有關,最後會形成這個東西
/** * Creates a module mapping that includes plugin prefix, module * name, and path. If parentModuleMap is provided it will * also normalize the name via require.normalize() * * @param {String} name the module name * @param {String} [parentModuleMap] parent module map * for the module name, used to resolve relative names. * @param {Boolean} isNormalized: is the ID already normalized. * This is true if this call is done for a define() module ID. * @param {Boolean} applyMap: apply the map config to the ID. * Should only be true if this map is for a dependency. * * @returns {Object} */ function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { var url, pluginModule, suffix, nameParts, prefix = null, parentName = parentModuleMap ? parentModuleMap.name : null, originalName = name, isDefine = true, normalizedName = ''; //If no name, then it means it is a require call, generate an //internal name. if (!name) { isDefine = false; name = '[email protected]' + (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 (pluginModule && pluginModule.normalize) { //Plugin is loaded, use its normalize method. normalizedName = pluginModule.normalize(name, function (name) { return normalize(name, parentName, applyMap); }); } else { normalizedName = normalize(name, parentName, applyMap); } } else { //A regular module. normalizedName = normalize(name, parentName, applyMap); //Normalized name may be a plugin ID due to map config //application in normalize. The map config values must //already be normalized, so do not need to redo that part. nameParts = splitPrefix(normalizedName); prefix = nameParts[0]; normalizedName = nameParts[1]; isNormalized = true; url = context.nameToUrl(normalizedName); } } //If the id is a plugin id that cannot be determined if it needs //normalization, stamp it with a unique ID so two matching relative //ids that may conflict can be separate. 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 }; }View Code
然後是我們關鍵的getModule函式
function getModule(depMap) { var id = depMap.id, mod = getOwn(registry, id); if (!mod) { mod = registry[id] = new context.Module(depMap); } return mod; }
可以看到,一旦我們載入了一個模組便不會重新載入了,這是一個很重要的發現哦
registry
該全域性變數用於儲存載入模組的鍵值對
第一步當然是載入啦,但是首次應該會跳過,因為當然事實上沒有需要載入的模組,一起跟下去吧
Module
然後進入我們關鍵的Module類模組了
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 = { init: function (depMaps, factory, errback, options) { options = options || {}; //Do not do more inits if already done. Can happen if there //are multiple define calls for the same module. That is not //a normal, common case, but it is also not unexpected. if (this.inited) { return; } this.factory = factory; if (errback) { //Register for errors on this module. this.on('error', errback); } else if (this.events.error) { //If no errback already, but there are error listeners //on this module, set up an errback to pass to the deps. errback = bind(this, function (err) { this.emit('error', err); }); } //Do a copy of the dependency array, so that //source inputs are not modified. For example //"shim" deps are passed in here directly, and //doing a direct modification of the depMaps array //would affect that config. this.depMaps = depMaps && depMaps.slice(0); this.errback = errback; //Indicate this module has be initialized this.inited = true; this.ignore = options.ignore; //Could have option to init this module in enabled mode, //or could have been previously marked as enabled. However, //the dependencies are not known until init is called. So //if enabled previously, now trigger dependencies as enabled. if (options.enabled || this.enabled) { //Enable this module and dependencies. //Will call this.check() this.enable(); } else { this.check(); } }, defineDep: function (i, depExports) { //Because of cycles, defined callback for a given //export can be called more than once. if (!this.depMatched[i]) { this.depMatched[i] = true; this.depCount -= 1; this.depExports[i] = depExports; } }, fetch: function () { if (this.fetched) { return; } this.fetched = true; context.startTime = (new Date()).getTime(); var map = this.map; //If the manager is for a plugin managed resource, //ask the plugin to load it now. if (this.shim) { context.makeRequire(this.map, { enableBuildCallback: true })(this.shim.deps || [], bind(this, function () { return map.prefix ? this.callPlugin() : this.load(); })); } else { //Regular dependency. 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); } }, /** * Checks if the module is ready to define itself, and if so, * define it. */ 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) { this.fetch(); } else if (this.error) { this.emit('error', this.error); } else if (!this.defining) { //The factory could trigger another require call //that would result in checking this module to //define itself again. If already in the process //of doing that, skip this work. this.defining = true; if (this.depCount < 1 && !this.defined) { if (isFunction(factory)) { //If there is an error listener, favor passing //to that instead of throwing an error. However, //only do it for define()'d modules. require //errbacks should not be called for failures in //their callbacks (#699). However if a global //onError is set, use that. if ((this.events.error && this.map.isDefine) || req.onError !== defaultOnError) { try { exports = context.execCb(id, factory, depExports, exports); } catch (e) { err = e; } } else { exports = context.execCb(id, factory, depExports, exports); } if (this.map.isDefine) { //If setting exports via 'module' is in play, //favor that over return value and exports. After that, //favor a non-undefined return value over exports use. cjsModule = this.module; if (cjsModule && cjsModule.exports !== undefined && //Make sure it is not already the exports value cjsModule.exports !== this.exports) { exports = cjsModule.exports; } else if (exports === undefined && this.usingExports) { //exports already set the defined value. exports = this.exports; } } if