參照上文的demo,咱們結合源碼分析在簡單的API調用的背後,到底使用了什麼技巧來實現各個模塊的依賴加載以及模塊API的導出。css
首先定義了一個Module類,對應與一個模塊node
function Module(uri, deps) { this.uri = uri this.dependencies = deps || [] this.exports = null this.status = 0 // Who depends on me this._waitings = {} // The number of unloaded dependencies this._remain = 0 }
Module有一些屬性,uri對應該模塊的絕對url,在Module.define函數中會有介紹;dependencies爲依賴模塊數組;exports爲導出的API;status爲當前的狀態碼;_waitings對象爲當前依賴該模塊的其餘模塊哈希表,其中key爲其餘模塊的url;_remain爲計數器,記錄還未加載的模塊個數。jquery
var STATUS = Module.STATUS = { // 1 - The `module.uri` is being fetched FETCHING: 1, // 2 - The meta data has been saved to cachedMods SAVED: 2, // 3 - The `module.dependencies` are being loaded LOADING: 3, // 4 - The module are ready to execute LOADED: 4, // 5 - The module is being executed EXECUTING: 5, // 6 - The `module.exports` is available EXECUTED: 6 }
上述爲狀態對象,記錄模塊的當前狀態:模塊初始化狀態爲0,當加載該模塊時,爲狀態fetching;模塊加載完畢而且緩存在cacheMods後,爲狀態saved;loading狀態意味着正在加載該模塊的其餘依賴模塊;loaded表示全部依賴模塊加載完畢,執行該模塊的回調函數,並設置依賴該模塊的其餘模塊是否還有依賴模塊未加載,若加載完畢執行回調函數;executing狀態表示該模塊正在執行;executed則是執行完畢,可使用exports的API。web
commonJS規範規定用define函數來定義一個模塊。define能夠接受1,2,3個參數都可,不過對於Module/wrappings規範而言,module.declare或者define函數只能接受一個參數,即工廠函數或者對象。不過原則上接受參數的個數並無本質上的區別,只不過庫在後臺給額外添加模塊名。數組
seajs鼓勵使用define(function(require,exports,module){})這種模塊定義方式,這是典型的Module/wrappings規範實現。可是在後臺經過解析工廠函數的require方法來獲取依賴模塊並給模塊設置id和url。瀏覽器
// Define a module Module.define = function (id, deps, factory) { var argsLen = arguments.length // define(factory) if (argsLen === 1) { factory = id id = undefined } else if (argsLen === 2) { factory = deps // define(deps, factory) if (isArray(id)) { deps = id id = undefined } // define(id, factory) else { deps = undefined } } // Parse dependencies according to the module factory code // 若是deps爲非數組,則序列化工廠函數獲取入參。 if (!isArray(deps) && isFunction(factory)) { deps = parseDependencies(factory.toString()) } var meta = { id: id, uri: Module.resolve(id), // 絕對url deps: deps, factory: factory } // Try to derive uri in IE6-9 for anonymous modules // 導出匿名模塊的uri if (!meta.uri && doc.attachEvent) { var script = getCurrentScript() if (script) { meta.uri = script.src } // NOTE: If the id-deriving methods above is failed, then falls back // to use onload event to get the uri } // Emit `define` event, used in nocache plugin, seajs node version etc emit("define", meta) meta.uri ? Module.save(meta.uri, meta) : // Save information for "saving" work in the script onload event anonymousMeta = meta }
模塊定義的最後,經過Module.save方法,將模塊保存到cachedMods緩存體中。緩存
parseDependencies方法比較巧妙的獲取依賴模塊。他經過函數的字符串表示,使用正則來獲取require(「…」)中的模塊名。app
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g var SLASH_RE = /\\\\/g function parseDependencies(code) { var ret = [] // 此處使用函數序列化(傳入的factory)進行字符串匹配,尋找require(「...」)的關鍵字 code.replace(SLASH_RE, "") .replace(REQUIRE_RE, function(m, m1, m2) { if (m2) { ret.push(m2) } }) return ret }
加載模塊能夠有多種方式,xhr方式能夠同步加載,也能夠異步加載,可是存在同源問題,所以難以在此使用。另外script tag方式在IE和現代瀏覽器下能夠保證並行加載和順序執行,script element方式也能夠保證並行加載但不保證順序執行,所以這兩種方式均可以使用。less
在seajs中,是採用script element方式來並行加載js/css資源的,並針對舊版本的webkit瀏覽器加載css作了hack。異步
function request(url, callback, charset) { var isCSS = IS_CSS_RE.test(url) var node = doc.createElement(isCSS ? "link" : "script") if (charset) { var cs = isFunction(charset) ? charset(url) : charset if (cs) { node.charset = cs } } // 添加 onload 函數。 addOnload(node, callback, isCSS, url) if (isCSS) { node.rel = "stylesheet" node.href = url } else { node.async = true node.src = url } // For some cache cases in IE 6-8, the script executes IMMEDIATELY after // the end of the insert execution, so use `currentlyAddingScript` to // hold current node, for deriving url in `define` call currentlyAddingScript = node // ref: #185 & http://dev.jquery.com/ticket/2709 baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node) currentlyAddingScript = null } function addOnload(node, callback, isCSS, url) { var supportOnload = "onload" in node // for Old WebKit and Old Firefox if (isCSS && (isOldWebKit || !supportOnload)) { setTimeout(function() { pollCss(node, callback) }, 1) // Begin after node insertion return } if (supportOnload) { node.onload = onload node.onerror = function() { emit("error", { uri: url, node: node }) onload() } } else { node.onreadystatechange = function() { if (/loaded|complete/.test(node.readyState)) { onload() } } } function onload() { // Ensure only run once and handle memory leak in IE node.onload = node.onerror = node.onreadystatechange = null // Remove the script to reduce memory leak if (!isCSS && !data.debug) { head.removeChild(node) } // Dereference the node node = null callback() } } // 針對 舊webkit和不支持onload的CSS節點判斷加載完畢的方法 function pollCss(node, callback) { var sheet = node.sheet var isLoaded // for WebKit < 536 if (isOldWebKit) { if (sheet) { isLoaded = true } } // for Firefox < 9.0 else if (sheet) { try { if (sheet.cssRules) { isLoaded = true } } catch (ex) { // The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR" // to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0 // in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR" if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") { isLoaded = true } } } setTimeout(function() { if (isLoaded) { // Place callback here to give time for style rendering callback() } else { pollCss(node, callback) } }, 20) }
其中有些細節還需注意,當採用script element方法插入script節點時,儘可能做爲首個子節點插入到head中,這是因爲一個難以發現的bug:
GLOBALEVAL WORKS INCORRECTLY IN IE6 IF THE CURRENT PAGE HAS <BASE HREF> TAG IN THE HEAD
初始化Module對象時,狀態爲0,該對象對應的js文件並未加載,若要加載js文件,須要使用上節提到的request方法,可是也不可能僅僅加載該文件,還須要設置module對象的狀態及其加載module依賴的其餘模塊。這些邏輯在fetch方法中得以體現:
// Fetch a module // 加載該模塊,fetch函數中調用了seajs.request函數 Module.prototype.fetch = function(requestCache) { var mod = this var uri = mod.uri mod.status = STATUS.FETCHING // Emit `fetch` event for plugins such as combo plugin var emitData = { uri: uri } emit("fetch", emitData) var requestUri = emitData.requestUri || uri // Empty uri or a non-CMD module if (!requestUri || fetchedList[requestUri]) { mod.load() return } if (fetchingList[requestUri]) { callbackList[requestUri].push(mod) return } fetchingList[requestUri] = true callbackList[requestUri] = [mod] // Emit `request` event for plugins such as text plugin emit("request", emitData = { uri: uri, requestUri: requestUri, onRequest: onRequest, charset: data.charset }) if (!emitData.requested) { requestCache ? requestCache[emitData.requestUri] = sendRequest : sendRequest() } function sendRequest() { seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset) } // 回調函數 function onRequest() { delete fetchingList[requestUri] fetchedList[requestUri] = true // Save meta data of anonymous module if (anonymousMeta) { Module.save(uri, anonymousMeta) anonymousMeta = null } // Call callbacks var m, mods = callbackList[requestUri] delete callbackList[requestUri] while ((m = mods.shift())) m.load() } }
其中seajs.request就是上節的request方法。onRequest做爲回調函數,做用是加載該模塊的其餘依賴模塊。
在下一節,將介紹模塊之間依賴的加載以及模塊的執行。