摘要:如果這個模塊的時候沒有設(shè)置,就表示是個匿名模塊,那怎么才能與之前發(fā)起請求的那個相匹配呢這里就有了一個全局變量,先將元數(shù)據(jù)放入這個對象。模塊加載完畢的回調(diào)保存元數(shù)據(jù)到匿名模塊,為請求的不管是不是匿名模塊,最后都是通過方法,將元數(shù)據(jù)存入到中。
近幾年前端工程化越來越完善,打包工具也已經(jīng)是前端標(biāo)配了,像seajs這種老古董早已停止維護,而且使用的人估計也幾個了。但這并不能阻止好奇的我,為了了解當(dāng)年的前端前輩們是如何在瀏覽器進行代碼模塊化的,我鼓起勇氣翻開了Seajs的源碼。下面就和我一起細細品味Seajs源碼吧。
如何使用seajs在看Seajs源碼之前,先看看Seajs是如何使用的,畢竟剛?cè)胄械臅r候,大家就都使用browserify、webpack之類的東西了,還從來沒有用過Seajs。
//main.js define(function (require, exports, module) { // require("jquery") // var $ = window.$ module.exports = "main-module" })seajs的參數(shù)配置
首先通過script導(dǎo)入seajs,然后對seajs進行一些配置。seajs的配置參數(shù)很多具體不詳細介紹,seajs將配置項會存入一個私有對象data中,并且如果之前有設(shè)置過某個屬性,并且這個屬性是數(shù)組或者對象,會將新值與舊值進行合并。
(function (global, undefined) { if (global.seajs) { return } var data = seajs.data = {} seajs.config = function (configData) { for (var key in configData) { var curr = configData[key] // 獲取當(dāng)前配置 var prev = data[key] // 獲取之前的配置 if (prev && isObject(prev)) { // 如果之前已經(jīng)設(shè)置過,且為一個對象 for (var k in curr) { prev[k] = curr[k] // 用新值覆蓋舊值,舊值保留不變 } } else { // 如果之前的值為數(shù)組,進行concat if (isArray(prev)) { curr = prev.concat(curr) } // 確保 base 為一個路徑 else if (key === "base") { // 必須已 "/" 結(jié)尾 if (curr.slice(-1) !== "/") { curr += "/" } curr = addBase(curr) // 轉(zhuǎn)換為絕對路徑 } // Set config data[key] = curr } } } })(this);
設(shè)置的時候還有個比較特殊的地方,就是base這個屬性。這表示所有模塊加載的基礎(chǔ)路徑,所以格式必須為一個路徑,并且該路徑最后會轉(zhuǎn)換為絕對路徑。比如,我的配置為base: "./js",我當(dāng)前訪問的域名為http://qq.com/web/index.html,最后base屬性會被轉(zhuǎn)化為http://qq.com/web/js/。然后,所有依賴的模塊id都會根據(jù)該路徑轉(zhuǎn)換為uri,除非有定義其他配置,關(guān)于配置點到為止,到用到的地方再來細說。
模塊的加載與執(zhí)行下面我們調(diào)用了use方法,該方法就是用來加載模塊的地方,類似與requirejs中的require方法。
// requirejs require(["main"], function (main) { console.log(main) });
只是這里的依賴項,seajs可以傳入字符串,而requirejs必須為一個數(shù)組,seajs會將字符串轉(zhuǎn)為數(shù)組,在內(nèi)部seajs.use會直接調(diào)用Module.use。這個Module為一個構(gòu)造函數(shù),里面掛載了所有與模塊加載相關(guān)的方法,還有很多靜態(tài)方法,比如實例化Module、轉(zhuǎn)換模塊id為uri、定義模塊等等,廢話不多說直接看代碼。
seajs.use = function(ids, callback) { Module.use(ids, callback, data.cwd + "_use_" + cid()) return seajs } // 該方法用來加載一個匿名模塊 Module.use = function (ids, callback, uri) { //如果是通過seajs.use調(diào)用,uri是自動生成的 var mod = Module.get( uri, isArray(ids) ? ids : [ids] // 這里會將依賴模塊轉(zhuǎn)成數(shù)組 ) mod._entry.push(mod) // 表示當(dāng)前模塊的入口為本身,后面還會把這個值傳入他的依賴模塊 mod.history = {} mod.remain = 1 // 這個值后面會用來標(biāo)識依賴模塊是否已經(jīng)全部加載完畢 mod.callback = function() { //設(shè)置模塊加載完畢的回調(diào),這一部分很重要,尤其是exec方法 var exports = [] var uris = mod.resolve() for (var i = 0, len = uris.length; i < len; i++) { exports[i] = cachedMods[uris[i]].exec() } if (callback) { callback.apply(global, exports) //執(zhí)行回調(diào) } } mod.load() }
這個use方法一共做了三件事:
調(diào)用Module.get,進行Module實例化
為模塊綁定回調(diào)函數(shù)
調(diào)用load,進行依賴模塊的加載
實例化模塊,一切的開端首先use方法調(diào)用了get靜態(tài)方法,這個方法是對Module進行實例化,并且將實例化的對象存入到全局對象cachedMods中進行緩存,并且以uri作為模塊的標(biāo)識,如果之后有其他模塊加載該模塊就能直接在緩存中獲取。
var cachedMods = seajs.cache = {} // 模塊的緩存對象 Module.get = function(uri, deps) { return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps)) } function Module(uri, deps) { this.uri = uri this.dependencies = deps || [] this.deps = {} // Ref the dependence modules this.status = 0 this._entry = [] }
綁定的回調(diào)函數(shù)會在所有模塊加載完畢之后調(diào)用,我們先跳過,直接看load方法。load方法會先把所有依賴的模塊id轉(zhuǎn)為uri,然后進行實例化,最后調(diào)用fetch方法,綁定模塊加載成功或失敗的回調(diào),最后進行模塊加載。具體代碼如下(代碼經(jīng)過精簡):
// 所有依賴加載完畢后執(zhí)行 onload Module.prototype.load = function() { var mod = this mod.status = STATUS.LOADING // 狀態(tài)置為模塊加載中 // 調(diào)用resolve方法,將模塊id轉(zhuǎn)為uri。 // 比如之前的"mian",會在前面加上我們之前設(shè)置的base,然后在后面拼上js后綴 // 最后變成: "http://qq.com/web/js/main.js" var uris = mod.resolve() // 遍歷所有依賴項的uri,然后進行依賴模塊的實例化 for (var i = 0, len = uris.length; i < len; i++) { mod.deps[mod.dependencies[i]] = Module.get(uris[i]) } // 將entry傳入到所有的依賴模塊,這個entry是我們在use方法的時候設(shè)置的 mod.pass() if (mod._entry.length) { mod.onload() return } // 開始進行并行加載 var requestCache = {} var m for (i = 0; i < len; i++) { m = cachedMods[uris[i]] // 獲取之前實例化的模塊對象 m.fetch(requestCache) // 進行fetch } // 發(fā)送請求進行模塊的加載 for (var requestUri in requestCache) { if (requestCache.hasOwnProperty(requestUri)) { requestCache[requestUri]() //調(diào)用 seajs.request } } }將模塊id轉(zhuǎn)為uri
resolve方法實現(xiàn)可以稍微看下,基本上是把config里面的參數(shù)拿出來,進行拼接uri的處理。
Module.prototype.resolve = function() { var mod = this var ids = mod.dependencies // 取出所有依賴模塊的id var uris = [] // 進行遍歷操作 for (var i = 0, len = ids.length; i < len; i++) { uris[i] = Module.resolve(ids[i], mod.uri) //將模塊id轉(zhuǎn)為uri } return uris } Module.resolve = function(id, refUri) { var emitData = { id: id, refUri: refUri } return seajs.resolve(emitData.id, refUri) // 調(diào)用 id2Uri } seajs.resolve = id2Uri function id2Uri(id, refUri) { // 將id轉(zhuǎn)為uri,轉(zhuǎn)換配置中的一些變量 if (!id) return "" id = parseAlias(id) id = parsePaths(id) id = parseAlias(id) id = parseVars(id) id = parseAlias(id) id = normalize(id) id = parseAlias(id) var uri = addBase(id, refUri) uri = parseAlias(uri) uri = parseMap(uri) return uri }
最后就是調(diào)用了id2Uri,將id轉(zhuǎn)為uri,其中調(diào)用了很多的parse方法,這些方法不一一去看,原理大致一樣,主要看下parseAlias。如果這個id有定義過alias,將alias取出,比如id為"jquery",之前在定義alias中又有定義jquery: "https://cdn.bootcss.com/jquery/3.2.1/jquery",則將id轉(zhuǎn)化為"https://cdn.bootcss.com/jquery/3.2.1/jquery"。代碼如下:
function parseAlias(id) { //如果有定義alias,將id替換為別名對應(yīng)的地址 var alias = data.alias return alias && isString(alias[id]) ? alias[id] : id }為依賴添加入口,方便追根溯源
resolve之后獲得uri,通過uri進行Module的實例化,然后調(diào)用pass方法,這個方法主要是記錄入口模塊到底有多少個未加載的依賴項,存入到remain中,并將entry都存入到依賴模塊的_entry屬性中,方便回溯。而這個remain用于計數(shù),最后onload的模塊數(shù)與remain相等就激活entry模塊的回調(diào)。具體代碼如下(代碼經(jīng)過精簡):
Module.prototype.pass = function() { var mod = this var len = mod.dependencies.length // 遍歷入口模塊的_entry屬性,這個屬性一般只有一個值,就是它本身 // 具體可以回去看use方法 -> mod._entry.push(mod) for (var i = 0; i < mod._entry.length; i++) { var entry = mod._entry[i] // 獲取入口模塊 var count = 0 // 計數(shù)器,用于統(tǒng)計未進行加載的模塊 for (var j = 0; j < len; j++) { var m = mod.deps[mod.dependencies[j]] //取出依賴的模塊 // 如果模塊未加載,并且在entry中未使用,將entry傳遞給依賴 if (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) { entry.history[m.uri] = true // 在入口模塊標(biāo)識曾經(jīng)加載過該依賴模塊 count++ m._entry.push(entry) // 將入口模塊存入依賴模塊的_entry屬性 } } // 如果未加載的依賴模塊大于0 if (count > 0) { // 這里`count - 1`的原因也可以回去看use方法 -> mod.remain = 1 // remain的初始值就是1,表示默認就會有一個未加載的模塊,所有需要減1 entry.remain += count - 1 // 如果有未加載的依賴項,則移除掉入口模塊的entry mod._entry.shift() i-- } } }如何發(fā)起請求,下載其他依賴模塊?
總的來說pass方法就是記錄了remain的數(shù)值,接下來就是重頭戲了,調(diào)用所有依賴項的fetch方法,然后進行依賴模塊的加載。調(diào)用fetch方法的時候會傳入一個requestCache對象,該對象用來緩存所有依賴模塊的request方法。
var requestCache = {} for (i = 0; i < len; i++) { m = cachedMods[uris[i]] // 獲取之前實例化的模塊對象 m.fetch(requestCache) // 進行fetch } Module.prototype.fetch = function(requestCache) { var mod = this var uri = mod.uri mod.status = STATUS.FETCHING callbackList[requestUri] = [mod] emit("request", emitData = { // 設(shè)置加載script時的一些數(shù)據(jù) uri: uri, requestUri: requestUri, onRequest: onRequest, charset: isFunction(data.charset) ? data.charset(requestUri) : data.charset, crossorigin: isFunction(data.crossorigin) ? data.crossorigin(requestUri) : data.crossorigin }) if (!emitData.requested) { //發(fā)送請求加載js文件 requestCache[emitData.requestUri] = sendRequest } function sendRequest() { // 被request方法,最終會調(diào)用 seajs.request seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset, emitData.crossorigin) } function onRequest(error) { //模塊加載完畢的回調(diào) var m, mods = callbackList[requestUri] delete callbackList[requestUri] // 保存元數(shù)據(jù)到匿名模塊,uri為請求js的uri if (anonymousMeta) { Module.save(uri, anonymousMeta) anonymousMeta = null } while ((m = mods.shift())) { // When 404 occurs, the params error will be true if(error === true) { m.error() } else { m.load() } } } }
經(jīng)過fetch操作后,能夠得到一個requestCache對象,該對象緩存了模塊的加載方法,從上面代碼就能看到,該方法最后調(diào)用的是seajs.request方法,并且傳入了一個onRequest回調(diào)。
for (var requestUri in requestCache) { requestCache[requestUri]() //調(diào)用 seajs.request } //用來加載js腳本的方法 seajs.request = request function request(url, callback, charset, crossorigin) { var node = doc.createElement("script") addOnload(node, callback, url) node.async = true //異步加載 node.src = url head.appendChild(node) } function addOnload(node, callback, url) { node.onload = onload node.onerror = function() { emit("error", { uri: url, node: node }) onload(true) } function onload(error) { node.onload = node.onerror = node.onreadystatechange = null // 腳本加載完畢的回調(diào) callback(error) } }通知入口模塊
上面就是request的邏輯,只不過刪除了一些兼容代碼,其實原理很簡單,和requirejs一樣,都是創(chuàng)建script標(biāo)簽,綁定onload事件,然后插入head中。在onload事件發(fā)生時,會調(diào)用之前fetch定義的onRequest方法,該方法最后會調(diào)用load方法。沒錯這個load方法又出現(xiàn)了,那么依賴模塊調(diào)用和入口模塊調(diào)用有什么區(qū)別呢,主要體現(xiàn)在下面代碼中:
if (mod._entry.length) { mod.onload() return }
如果這個依賴模塊沒有另外的依賴模塊,那么他的entry就會存在,然后調(diào)用onload模塊,但是如果這個代碼中有define方法,并且還有其他依賴項,就會走上面那么邏輯,遍歷依賴項,轉(zhuǎn)換uri,調(diào)用fetch巴拉巴拉。這個后面再看,先看看onload會做什么。
Module.prototype.onload = function() { var mod = this mod.status = STATUS.LOADED for (var i = 0, len = (mod._entry || []).length; i < len; i++) { var entry = mod._entry[i] // 每次加載完畢一個依賴模塊,remain就-1 // 直到remain為0,就表示所有依賴模塊加載完畢 if (--entry.remain === 0) { // 最后就會調(diào)用entry的callback方法 // 這就是前面為什么要給每個依賴模塊存入entry entry.callback() } } delete mod._entry }依賴模塊執(zhí)行,完成全部操作
還記得最開始use方法中給入口模塊設(shè)置callback方法嗎,沒錯,兜兜轉(zhuǎn)轉(zhuǎn)我們又回到了起點。
mod.callback = function() { //設(shè)置模塊加載完畢的回調(diào) var exports = [] var uris = mod.resolve() for (var i = 0, len = uris.length; i < len; i++) { // 執(zhí)行所有依賴模塊的exec方法,存入exports數(shù)組 exports[i] = cachedMods[uris[i]].exec() } if (callback) { callback.apply(global, exports) //執(zhí)行回調(diào) } // 移除一些屬性 delete mod.callback delete mod.history delete mod.remain delete mod._entry }
那么這個exec到底做了什么呢?
Module.prototype.exec = function () { var mod = this mod.status = STATUS.EXECUTING if (mod._entry && !mod._entry.length) { delete mod._entry } function require(id) { var m = mod.deps[id] return m.exec() } var factory = mod.factory // 調(diào)用define定義的回調(diào) // 傳入commonjs相關(guān)三個參數(shù): require, module.exports, module var exports = factory.call(mod.exports = {}, require, mod.exports, mod) if (exports === undefined) { exports = mod.exports //如果函數(shù)沒有返回值,就取mod.exports } mod.exports = exports mod.status = STATUS.EXECUTED return mod.exports // 返回模塊的exports }
這里的factory就是依賴模塊define中定義的回調(diào)函數(shù),例如我們加載的main.js中,定義了一個模塊。
define(function (require, exports, module) { module.exports = "main-module" })
那么調(diào)用這個factory的時候,exports就為module.exports,也是是字符串"main-moudle"。最后callback傳入的參數(shù)就是"main-moudle"。所以我們執(zhí)行最開頭寫的那段代碼,最后會在頁面上彈出main-moudle。
define定義模塊你以為到這里就結(jié)束了嗎?并沒有。前面只說了加載依賴模塊中define方法中沒有其他依賴,那如果有其他依賴呢?廢話不多說,先看看define方法做了什么:
global.define = Module.define Module.define = function (id, deps, factory) { var argsLen = arguments.length // 參數(shù)校準 if (argsLen === 1) { factory = id id = undefined } else if (argsLen === 2) { factory = deps if (isArray(id)) { deps = id id = undefined } else { deps = undefined } } // 如果沒有直接傳入依賴數(shù)組 // 則從factory中提取所有的依賴模塊到dep數(shù)組中 if (!isArray(deps) && isFunction(factory)) { deps = typeof parseDependencies === "undefined" ? [] : parseDependencies(factory.toString()) } var meta = { //模塊加載與定義的元數(shù)據(jù) id: id, uri: Module.resolve(id), deps: deps, factory: factory } // 激活define事件, used in nocache plugin, seajs node version etc emit("define", meta) meta.uri ? Module.save(meta.uri, meta) : // 在腳本加載完畢的onload事件進行save anonymousMeta = meta }
首先進行了參數(shù)的修正,這個邏輯很簡單,直接跳過。第二步判斷了有沒有依賴數(shù)組,如果沒有,就通過parseDependencies方法從factory中獲取。這個方法很有意思,是一個狀態(tài)機,會一步步的去解析字符串,匹配到require,將其中的模塊取出,最后放到一個數(shù)組里。這個方法在requirejs中是通過正則實現(xiàn)的,早期seajs也是通過正則匹配的,后來改成了這種狀態(tài)機的方式,可能是考慮到性能的問題。seajs的倉庫中專門有一個模塊來講這個東西的,請看鏈接。
獲取到依賴模塊之后又設(shè)置了一個meta對象,這個就表示這個模塊的原數(shù)據(jù),里面有記錄模塊的依賴項、id、factory等。如果這個模塊define的時候沒有設(shè)置id,就表示是個匿名模塊,那怎么才能與之前發(fā)起請求的那個mod相匹配呢?
這里就有了一個全局變量anonymousMeta,先將元數(shù)據(jù)放入這個對象。然后回過頭看看模塊加載時設(shè)置的onload函數(shù)里面有一段就是獲取這個全局變量的。
function onRequest(error) { //模塊加載完畢的回調(diào) ... // 保存元數(shù)據(jù)到匿名模塊,uri為請求js的uri if (anonymousMeta) { Module.save(uri, anonymousMeta) anonymousMeta = null } ... }
不管是不是匿名模塊,最后都是通過save方法,將元數(shù)據(jù)存入到mod中。
// 存儲元數(shù)據(jù)到 cachedMods 中 Module.save = function(uri, meta) { var mod = Module.get(uri) if (mod.status < STATUS.SAVED) { mod.id = meta.id || uri mod.dependencies = meta.deps || [] mod.factory = meta.factory mod.status = STATUS.SAVED } }
這里完成之后,就是和前面的邏輯一樣了,先去校驗當(dāng)前模塊有沒有依賴項,如果有依賴項,就去加載依賴項和use的邏輯是一樣的,等依賴項全部加載完畢后,通知入口模塊的remain減1,知道remain為0,最后調(diào)用入口模塊的回調(diào)方法。整個seajs的邏輯就已經(jīng)全部走通,Yeah!
結(jié)語有過看requirejs的經(jīng)驗,再來看seajs還是順暢很多,對模塊化的理解有了更加深刻的理解。閱讀源碼之前還是得對框架有個基本認識,并且有使用過,要不然很多地方都很懵懂。所以以后還是閱讀一些工作中有經(jīng)常使用的框架或類庫的源碼進行閱讀,不能總像個無頭蒼蠅一樣。
最后用一張流程圖,總結(jié)下seajs的加載過程。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96924.html
摘要:本文主要簡單地解讀一下的源碼和模塊化原理。其中,是這次源碼解讀的核心,但我也會順帶介紹一下其他文件的作用的。對代碼比較簡單,其實就是聲明一下全局的命名空間。然而,真正的核心在于處理模塊依賴的問題。 seajs 簡單介紹 seajs是前端應(yīng)用模塊化開發(fā)的一種很好的解決方案。對于多人協(xié)作開發(fā)的、復(fù)雜龐大的前端項目尤其有用。簡單的介紹不多說,大家可以到seajs的官網(wǎng)seajs.org參看...
摘要:這里的依賴都是通過來異步加載的,加載完畢之后立刻執(zhí)行函數(shù),在模塊文件執(zhí)行完畢后包括和其他代碼,觸發(fā)的事件。 入口 seajs.use seajs.use直接調(diào)用Module.use(),Module.use的源碼如下: // Use function is equal to load a anonymous module // ids:模塊標(biāo)識,uri是dirname + _us...
摘要:所有依賴這個模塊的語句,都定義在一個回調(diào)函數(shù)中,等到所有依賴加載完成之后前置依賴,這個回調(diào)函數(shù)才會運行。如果將前面的代碼改寫成形式,就是下面這樣定義了一個文件,該文件依賴模塊,當(dāng)模塊加載完畢之后執(zhí)行回調(diào)函數(shù),這里并沒有暴露任何變量。 模塊化是我們?nèi)粘i_發(fā)都要用到的基本技能,使用簡單且方便,但是很少人能說出來但是的原因及發(fā)展過程?,F(xiàn)在通過對比不同時期的js的發(fā)展,將JavaScript模...
摘要:依賴信息是一個數(shù)組,比如上面的依賴數(shù)組是源碼如下是利用正則解析依賴的一個函數(shù)時間出發(fā)函數(shù)主要看這個部分注釋是防止拷貝該時間的回調(diào)函數(shù),防止修改,困惑了一下。對的賦值需要同步執(zhí)行,不能放在回調(diào)函數(shù)里。 sea.js想解決的問題 惱人的命名沖突 煩瑣的文件依賴 對應(yīng)帶來的好處 Sea.js 帶來的兩大好處: 通過 exports 暴露接口。這意味著不需要命名空間了,更不需要全局變量。...
閱讀 3783·2021-11-23 09:51
閱讀 4421·2021-11-15 11:37
閱讀 3534·2021-09-02 15:21
閱讀 2756·2021-09-01 10:31
閱讀 887·2021-08-31 14:19
閱讀 865·2021-08-11 11:20
閱讀 3318·2021-07-30 15:30
閱讀 1699·2019-08-30 15:54