本文將簡單介紹下我的對require.js的源碼分析,簡單分析實現原理javascript
一.require加載資源的流程html
require中,根據AMD(Asynchronous Module Definition)的思想,即異步模塊加載機制,其思想就是把代碼分爲一個個的模塊來分塊按需加載,這樣咱們能夠組裝不少UI或者功能組件,從而實現代碼的複用性。java
1.data-main 函數從requirejs方法開始調用,newContext方法調用makeRequire方法。node
1.1 makeRequire方法判斷module是否defined,若是是yes就調用localRequire方法 其中能夠經過getModule獲取已保存的資源api
1.2 若是module沒有defined,調用makeRequire方法經過 遞歸 迭代 不停的註冊module 而後放入makeModuleMap中,調用callGetModule方法緩存
1.3 callGetModule方法只用來加載js資源的 module.init 調用 module.fetch 調用 module.load 最後調用 req.load req.load 在遍歷 調用req.createNodeapp
1.4 其中有個checkLoaded方法使用定時器不停掃描資源加載狀態,這裏有3種狀態異步
1.4.1 stillLoading 還在加載 enabling 對該模塊的依賴進行加載和模塊化 defining 對正在處理的模塊進行加載,並運行模塊中的callback enabled處理完成的async
若是判斷js加載完成,調用原生js的一些監聽事件 如:onreadystatechange 模塊化
req.load = function (context, moduleName, url) { var config = (context && context.config) || {}, node; if (isBrowser) { //In the browser so use a script tag node = req.createNode(config, moduleName, url); node.setAttribute('data-requirecontext', context.contextName); node.setAttribute('data-requiremodule', moduleName); if (node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera) { useInteractive = true; node.attachEvent('onreadystatechange', context.onScriptLoad); } else { node.addEventListener('load', context.onScriptLoad, false); node.addEventListener('error', context.onScriptError, false); } node.src = url; if (config.onNodeCreated) { config.onNodeCreated(node, config, moduleName, url); } currentlyAddingScript = node; if (baseElement) { head.insertBefore(node, baseElement); } else { head.appendChild(node); } currentlyAddingScript = null; return node;
...
如何引入資源 ,實際上是插入標籤,async等於true,
req.createNode = function (config, moduleName, url) { var node = config.xhtml ? document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : document.createElement('script'); node.type = config.scriptType || 'text/javascript'; node.charset = 'utf-8'; node.async = true; return node; };
但事實是咱們在查看源代碼的時候沒有看到不少<script>標籤,由於require.js最後在加載完成後刪除了,在checkLoaded方法種會掃描資源加載狀態,最後把加載完畢的資源刪除。
function removeScript(name) { if (isBrowser) { each(scripts(), function (scriptNode) { if (scriptNode.getAttribute('data-requiremodule') === name && scriptNode.getAttribute('data-requirecontext') === context.contextName) { scriptNode.parentNode.removeChild(scriptNode); return true; } }); } }
...
if (!mod.inited && expired) { if (hasPathFallback(modId)) { usingPathFallback = true; stillLoading = true; } else { noLoads.push(modId); removeScript(modId); } }
...
二.require定義模塊,這個函數根據參數類型和個數,最後定義module的依賴和回調,還記得官網大篇幅的文檔若是定義module吧
http://requirejs.org/docs/api.html#define,一一揣摩就明白參數和回調的堆棧。
define = function (name, deps, callback) { var node, context; //Allow for anonymous modules if (typeof name !== 'string') { //Adjust args appropriately callback = deps; deps = name; name = null; } //This module may not have dependencies if (!isArray(deps)) { callback = deps; deps = null; } //If no name, and callback is a function, then figure out if it a //CommonJS thing with dependencies. if (!deps && isFunction(callback)) { deps = []; if (callback.length) { callback .toString() .replace(commentRegExp, commentReplace) .replace(cjsRequireRegExp, function (match, dep) { deps.push(dep); }); deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); } } if (useInteractive) { node = currentlyAddingScript || getInteractiveScript(); if (node) { if (!name) { name = node.getAttribute('data-requiremodule'); } context = contexts[node.getAttribute('data-requirecontext')]; } } if (context) { context.defQueue.push([name, deps, callback]); context.defQueueMap[name] = true; } else { globalDefQueue.push([name, deps, callback]); } }; define.amd = { jQuery: true };
在這裏用到了2個很長的正則
commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg, 替換回調種的註釋
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, 匹配require中的參數,把產生提取出來,後面在判斷是否Common.js來拼接Commonjs的三元素
最後從新附加到參數隊尾。
三.require方法內部的主要功能實現
3.1 加載依賴資源何時才結束呢,這裏require會在load方法中 觸發js加載事件 回調onScriptLoad
onScriptLoad方法會調用completeLoad方法,
completeLoad: function (moduleName) { var found, args, mod, shim = getOwn(config.shim, moduleName) || {}, shExports = shim.exports; takeGlobalQueue(); while (defQueue.length) { args = defQueue.shift(); if (args[0] === null) { args[0] = moduleName; //If already found an anonymous module and bound it //to this name, then this is some other anon module //waiting for its completeLoad to fire. if (found) { break; } found = true; } else if (args[0] === moduleName) { //Found matching define call for this script! found = true; } callGetModule(args); } context.defQueueMap = {}; //Do this after the cycle of callGetModule in case the result //of those calls/init calls changes the registry. mod = getOwn(registry, moduleName); if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { if (hasPathFallback(moduleName)) { return; } else { return onError(makeError('nodefine', 'No define call for ' + moduleName, null, [moduleName])); } } else { //A script that does not call define(), so just simulate //the call for it. callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); } } checkLoaded(); },
這個函數主要 遍歷defQueue,獲取一個module,傳遞moduleName,並調用callGetModule
去調用模塊
function callGetModule(args) { //Skip modules already defined. if (!hasProp(defined, args[0])) { getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); } }
callGetModule會調用函數判斷是否有這個moduleName的module是否認義了,若是沒定義使用makeModuleMap建立一個module返回
在makeModuleMap能夠看到一個module有那些屬性,爲了保證moduleId惟一性,能夠看到有前綴後綴,以及id拼接規則。
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 };
getModule方法有是如何實現的,在require內部定義了Module類
,而這個方法則會爲當前的ModuleMap中,其中包含了這個模塊的路徑等信息。這裏要注意的是getModule方法裏面擁有一個基於
registry變量中,這裏則是用緩存根據ModuleMap來實例化的Module,並將其保存在了registry變量中。
這裏咱們也能夠看到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; }; Module.prototype = { //init Module init : function (depMaps, factory, errback, options) {}, //define dependencies defineDep : function (i, depExports) {}, //call require for plugins fetch : function () {}, //use script to load js load : function () {}, //Checks if the module is ready to define itself, and if so, define it. check : function () {}, //call Plugins if them exist and defines them callPlugin : function () {}, //enable dependencies and call defineDep enable : function () {}, //register event on : function (name, cb) {}, //trigger event emit : function (name, evt) {} }
建立一個Module的時候,會使用init方法,其中enable和check方法是module重要的方法 ,enable遍歷調用check方法,所有依賴都檢查無誤以後,改變(資源加載中)enabling狀態爲false,
check方法不停的遍歷依賴,最後改變module狀態defined爲真。
3.2多層依賴如何加載,重複加載的問題如何解決,好比 A依賴 BC ,B依賴 CD C依賴 DE
當若是我去requrie(A)時,require去查找defined中是否有A模塊,若是沒有,則去調用makeModuleMap來爲即將調用的模塊實例一個ModuleMap並加入到defined中,再用ModuleMap實例化一個Module加入到registry中,可是這時候的Module是一個空殼,它是隻存儲了一些模塊相關的依賴等,模塊裏的exports或者callback是尚未被嵌進來,由於這個文件根本沒有被加載
只有在觸發module.init方法的時候纔會真正的加載資源文件,可是如何保證加載是否重複,這時候會用到chekLoaded()方法,這個方法會檢查依賴是否已經define的
function checkLoaded() { var err, usingPathFallback, waitInterval = config.waitSeconds * 1000, //It is possible to disable the wait interval by using waitSeconds of 0. expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), noLoads = [], reqCalls = [], stillLoading = false, needCycleCheck = true; //Do not bother if this call was a result of a cycle break. if (inCheckLoaded) { return; } inCheckLoaded = true; //Figure out the state of all the modules. eachProp(enabledRegistry, function (mod) { var map = mod.map, modId = map.id; //Skip things that are not enabled or in error state. if (!mod.enabled) { return; } if (!map.isDefine) { reqCalls.push(mod); } if (!mod.error) { //If the module should be executed, and it has not //been inited and time is up, remember it. if (!mod.inited && expired) { if (hasPathFallback(modId)) { usingPathFallback = true; stillLoading = true; } else { noLoads.push(modId); removeScript(modId); } } else if (!mod.inited && mod.fetched && map.isDefine) { stillLoading = true; if (!map.prefix) { //No reason to keep looking for unfinished //loading. If the only stillLoading is a //plugin resource though, keep going, //because it may be that a plugin resource //is waiting on a non-plugin cycle. return (needCycleCheck = false); } } } }); if (expired && noLoads.length) { //If wait time expired, throw error of unloaded modules. err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); err.contextName = context.contextName; return onError(err); } //Not expired, check for a cycle. if (needCycleCheck) { each(reqCalls, function (mod) { breakCycle(mod, {}, {}); }); } //If still waiting on loads, and the waiting load is something //other than a plugin resource, or there are still outstanding //scripts, then just try back later. if ((!expired || usingPathFallback) && stillLoading) { //Something is still waiting to load. Wait for it, but only //if a timeout is not already in effect. if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { checkLoadedTimeoutId = setTimeout(function () { checkLoadedTimeoutId = 0; checkLoaded(); }, 50); } } inCheckLoaded = false; }
定義依賴方法,官網推薦使用
define(dependencies, callback)
而不使用
define( callback(
require(a);
require(b);
));
第二種方法會先把callback.toString,而後查找require,效率上差了些。
無論是第一種仍是第二種,都必須依賴加載完成的時候才能回調,因此平常工做中咱們須要肯定依賴的前後順序,一些主要暫時模塊須要優先加載,一些不影響主頁面的東西依賴的優先級要放低,依賴要原子項,不能多方依賴,多方依賴致使僅僅一個小功能須要加載不少額外的東西