近幾年前端工程化越來(lái)越完善,打包工具也已經(jīng)是前端標(biāo)配了,像seajs這種老古董早已停止維護(hù),而且使用的人估計(jì)也幾個(gè)了。但這并不能阻止好奇的我,為了了解當(dāng)年的前端前輩們是如何在瀏覽器進(jìn)行代碼模塊化的,我鼓起勇氣翻開(kāi)了Seajs的源碼。下面就和我一起細(xì)細(xì)品味Seajs源碼吧。
創(chuàng)新互聯(lián)基于成都重慶香港及美國(guó)等地區(qū)分布式IDC機(jī)房數(shù)據(jù)中心構(gòu)建的電信大帶寬,聯(lián)通大帶寬,移動(dòng)大帶寬,多線BGP大帶寬租用,是為眾多客戶(hù)提供專(zhuān)業(yè)成都天府聯(lián)通服務(wù)器托管報(bào)價(jià),主機(jī)托管價(jià)格性?xún)r(jià)比高,為金融證券行業(yè)服務(wù)器托管,ai人工智能服務(wù)器托管提供bgp線路100M獨(dú)享,G口帶寬及機(jī)柜租用的專(zhuān)業(yè)成都idc公司。
如何使用seajs
在看Seajs源碼之前,先看看Seajs是如何使用的,畢竟剛?cè)胄械臅r(shí)候,大家就都使用browserify、webpack之類(lèi)的東西了,還從來(lái)沒(méi)有用過(guò)Seajs。
//main.js define(function (require, exports, module) { // require('jquery') // var $ = window.$ module.exports = 'main-module' })
seajs的參數(shù)配置
首先通過(guò)script導(dǎo)入seajs,然后對(duì)seajs進(jìn)行一些配置。seajs的配置參數(shù)很多具體不詳細(xì)介紹,seajs將配置項(xiàng)會(huì)存入一個(gè)私有對(duì)象data中,并且如果之前有設(shè)置過(guò)某個(gè)屬性,并且這個(gè)屬性是數(shù)組或者對(duì)象,會(huì)將新值與舊值進(jìn)行合并。
(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è)置過(guò),且為一個(gè)對(duì)象 for (var k in curr) { prev[k] = curr[k] // 用新值覆蓋舊值,舊值保留不變 } } else { // 如果之前的值為數(shù)組,進(jìn)行concat if (isArray(prev)) { curr = prev.concat(curr) } // 確保 base 為一個(gè)路徑 else if (key === "base") { // 必須已 "/" 結(jié)尾 if (curr.slice(-1) !== "/") { curr += "/" } curr = addBase(curr) // 轉(zhuǎn)換為絕對(duì)路徑 } // Set config data[key] = curr } } } })(this);
設(shè)置的時(shí)候還有個(gè)比較特殊的地方,就是base這個(gè)屬性。這表示所有模塊加載的基礎(chǔ)路徑,所以格式必須為一個(gè)路徑,并且該路徑最后會(huì)轉(zhuǎn)換為絕對(duì)路徑。比如,我的配置為base: './js'
,我當(dāng)前訪問(wèn)的域名為http://qq.com/web/index.html
,最后base屬性會(huì)被轉(zhuǎn)化為http://qq.com/web/js/
。然后,所有依賴(lài)的模塊id都會(huì)根據(jù)該路徑轉(zhuǎn)換為uri,除非有定義其他配置,關(guān)于配置點(diǎn)到為止,到用到的地方再來(lái)細(xì)說(shuō)。
模塊的加載與執(zhí)行
下面我們調(diào)用了use方法,該方法就是用來(lái)加載模塊的地方,類(lèi)似與requirejs中的require方法。
// requirejs require(['main'], function (main) { console.log(main) });
只是這里的依賴(lài)項(xiàng),seajs可以傳入字符串,而requirejs必須為一個(gè)數(shù)組,seajs會(huì)將字符串轉(zhuǎn)為數(shù)組,在內(nèi)部seajs.use會(huì)直接調(diào)用Module.use。這個(gè)Module為一個(gè)構(gòu)造函數(shù),里面掛載了所有與模塊加載相關(guān)的方法,還有很多靜態(tài)方法,比如實(shí)例化Module、轉(zhuǎn)換模塊id為uri、定義模塊等等,廢話(huà)不多說(shuō)直接看代碼。
seajs.use = function(ids, callback) { Module.use(ids, callback, data.cwd + "_use_" + cid()) return seajs } // 該方法用來(lái)加載一個(gè)匿名模塊 Module.use = function (ids, callback, uri) { //如果是通過(guò)seajs.use調(diào)用,uri是自動(dòng)生成的 var mod = Module.get( uri, isArray(ids) ? ids : [ids] // 這里會(huì)將依賴(lài)模塊轉(zhuǎn)成數(shù)組 ) mod._entry.push(mod) // 表示當(dāng)前模塊的入口為本身,后面還會(huì)把這個(gè)值傳入他的依賴(lài)模塊 mod.history = {} mod.remain = 1 // 這個(gè)值后面會(huì)用來(lái)標(biāo)識(shí)依賴(lài)模塊是否已經(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() }
這個(gè)use方法一共做了三件事:
1.調(diào)用Module.get,進(jìn)行Module實(shí)例化
2.為模塊綁定回調(diào)函數(shù)
3.調(diào)用load,進(jìn)行依賴(lài)模塊的加載
實(shí)例化模塊,一切的開(kāi)端
首先use方法調(diào)用了get靜態(tài)方法,這個(gè)方法是對(duì)Module進(jìn)行實(shí)例化,并且將實(shí)例化的對(duì)象存入到全局對(duì)象cachedMods中進(jìn)行緩存,并且以u(píng)ri作為模塊的標(biāo)識(shí),如果之后有其他模塊加載該模塊就能直接在緩存中獲取。
var cachedMods = seajs.cache = {} // 模塊的緩存對(duì)象 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ù)會(huì)在所有模塊加載完畢之后調(diào)用,我們先跳過(guò),直接看load方法。load方法會(huì)先把所有依賴(lài)的模塊id轉(zhuǎn)為uri,然后進(jìn)行實(shí)例化,最后調(diào)用fetch方法,綁定模塊加載成功或失敗的回調(diào),最后進(jìn)行模塊加載。具體代碼如下(代碼經(jīng)過(guò)精簡(jiǎn))
:
// 所有依賴(lài)加載完畢后執(zhí)行 onload Module.prototype.load = function() { var mod = this mod.status = STATUS.LOADING // 狀態(tài)置為模塊加載中 // 調(diào)用resolve方法,將模塊id轉(zhuǎn)為uri。 // 比如之前的"mian",會(huì)在前面加上我們之前設(shè)置的base,然后在后面拼上js后綴 // 最后變成: "http://qq.com/web/js/main.js" var uris = mod.resolve() // 遍歷所有依賴(lài)項(xiàng)的uri,然后進(jìn)行依賴(lài)模塊的實(shí)例化 for (var i = 0, len = uris.length; i < len; i++) { mod.deps[mod.dependencies[i]] = Module.get(uris[i]) } // 將entry傳入到所有的依賴(lài)模塊,這個(gè)entry是我們?cè)趗se方法的時(shí)候設(shè)置的 mod.pass() if (mod._entry.length) { mod.onload() return } // 開(kāi)始進(jìn)行并行加載 var requestCache = {} var m for (i = 0; i < len; i++) { m = cachedMods[uris[i]] // 獲取之前實(shí)例化的模塊對(duì)象 m.fetch(requestCache) // 進(jìn)行fetch } // 發(fā)送請(qǐng)求進(jìn)行模塊的加載 for (var requestUri in requestCache) { if (requestCache.hasOwnProperty(requestUri)) { requestCache[requestUri]() //調(diào)用 seajs.request } } }
將模塊id轉(zhuǎn)為uri
resolve方法實(shí)現(xiàn)可以稍微看下,基本上是把config里面的參數(shù)拿出來(lái),進(jìn)行拼接uri的處理。
Module.prototype.resolve = function() { var mod = this var ids = mod.dependencies // 取出所有依賴(lài)模塊的id var uris = [] // 進(jìn)行遍歷操作 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
。如果這個(gè)id有定義過(guò)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替換為別名對(duì)應(yīng)的地址 var alias = data.alias return alias && isString(alias[id]) ? alias[id] : id }
為依賴(lài)添加入口,方便追根溯源
resolve之后獲得uri,通過(guò)uri進(jìn)行Module的實(shí)例化,然后調(diào)用pass方法,這個(gè)方法主要是記錄入口模塊到底有多少個(gè)未加載的依賴(lài)項(xiàng),存入到remain中,并將entry都存入到依賴(lài)模塊的_entry屬性中,方便回溯。而這個(gè)remain用于計(jì)數(shù),最后onload的模塊數(shù)與remain相等就激活entry模塊的回調(diào)。具體代碼如下(代碼經(jīng)過(guò)精簡(jiǎn))
:
Module.prototype.pass = function() { var mod = this var len = mod.dependencies.length // 遍歷入口模塊的_entry屬性,這個(gè)屬性一般只有一個(gè)值,就是它本身 // 具體可以回去看use方法 -> mod._entry.push(mod) for (var i = 0; i < mod._entry.length; i++) { var entry = mod._entry[i] // 獲取入口模塊 var count = 0 // 計(jì)數(shù)器,用于統(tǒng)計(jì)未進(jìn)行加載的模塊 for (var j = 0; j < len; j++) { var m = mod.deps[mod.dependencies[j]] //取出依賴(lài)的模塊 // 如果模塊未加載,并且在entry中未使用,將entry傳遞給依賴(lài) if (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) { entry.history[m.uri] = true // 在入口模塊標(biāo)識(shí)曾經(jīng)加載過(guò)該依賴(lài)模塊 count++ m._entry.push(entry) // 將入口模塊存入依賴(lài)模塊的_entry屬性 } } // 如果未加載的依賴(lài)模塊大于0 if (count > 0) { // 這里`count - 1`的原因也可以回去看use方法 -> mod.remain = 1 // remain的初始值就是1,表示默認(rèn)就會(huì)有一個(gè)未加載的模塊,所有需要減1 entry.remain += count - 1 // 如果有未加載的依賴(lài)項(xiàng),則移除掉入口模塊的entry mod._entry.shift() i-- } } }
如何發(fā)起請(qǐng)求,下載其他依賴(lài)模塊?
總的來(lái)說(shuō)pass方法就是記錄了remain的數(shù)值,接下來(lái)就是重頭戲了,調(diào)用所有依賴(lài)項(xiàng)的fetch方法,然后進(jìn)行依賴(lài)模塊的加載。調(diào)用fetch方法的時(shí)候會(huì)傳入一個(gè)requestCache對(duì)象,該對(duì)象用來(lái)緩存所有依賴(lài)模塊的request方法。
var requestCache = {} for (i = 0; i < len; i++) { m = cachedMods[uris[i]] // 獲取之前實(shí)例化的模塊對(duì)象 m.fetch(requestCache) // 進(jìn)行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í)的一些數(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ā)送請(qǐng)求加載js文件 requestCache[emitData.requestUri] = sendRequest } function sendRequest() { // 被request方法,最終會(huì)調(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為請(qǐng)求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)過(guò)fetch操作后,能夠得到一個(gè)requestCache
對(duì)象,該對(duì)象緩存了模塊的加載方法,從上面代碼就能看到,該方法最后調(diào)用的是seajs.request
方法,并且傳入了一個(gè)onRequest回調(diào)。
for (var requestUri in requestCache) { requestCache[requestUri]() //調(diào)用 seajs.request } //用來(lái)加載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的邏輯,只不過(guò)刪除了一些兼容代碼,其實(shí)原理很簡(jiǎn)單,和requirejs一樣,都是創(chuàng)建script標(biāo)簽,綁定onload事件,然后插入head中。在onload事件發(fā)生時(shí),會(huì)調(diào)用之前fetch定義的onRequest方法,該方法最后會(huì)調(diào)用load方法。沒(méi)錯(cuò)這個(gè)load方法又出現(xiàn)了,那么依賴(lài)模塊調(diào)用和入口模塊調(diào)用有什么區(qū)別呢,主要體現(xiàn)在下面代碼中:
if (mod._entry.length) { mod.onload() return }
如果這個(gè)依賴(lài)模塊沒(méi)有另外的依賴(lài)模塊,那么他的entry就會(huì)存在,然后調(diào)用onload模塊,但是如果這個(gè)代碼中有define
方法,并且還有其他依賴(lài)項(xiàng),就會(huì)走上面那么邏輯,遍歷依賴(lài)項(xiàng),轉(zhuǎn)換uri,調(diào)用fetch巴拉巴拉。這個(gè)后面再看,先看看onload會(huì)做什么。
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] // 每次加載完畢一個(gè)依賴(lài)模塊,remain就-1 // 直到remain為0,就表示所有依賴(lài)模塊加載完畢 if (--entry.remain === 0) { // 最后就會(huì)調(diào)用entry的callback方法 // 這就是前面為什么要給每個(gè)依賴(lài)模塊存入entry entry.callback() } } delete mod._entry }
依賴(lài)模塊執(zhí)行,完成全部操作
還記得最開(kāi)始use方法中給入口模塊設(shè)置callback方法嗎,沒(méi)錯(cuò),兜兜轉(zhuǎn)轉(zhuǎn)我們又回到了起點(diǎn)。
mod.callback = function() { //設(shè)置模塊加載完畢的回調(diào) var exports = [] var uris = mod.resolve() for (var i = 0, len = uris.length; i < len; i++) { // 執(zhí)行所有依賴(lài)模塊的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 }
那么這個(gè)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)三個(gè)參數(shù): require, module.exports, module var exports = factory.call(mod.exports = {}, require, mod.exports, mod) if (exports === undefined) { exports = mod.exports //如果函數(shù)沒(méi)有返回值,就取mod.exports } mod.exports = exports mod.status = STATUS.EXECUTED return mod.exports // 返回模塊的exports }
這里的factory就是依賴(lài)模塊define中定義的回調(diào)函數(shù),例如我們加載的main.js
中,定義了一個(gè)模塊。
define(function (require, exports, module) { module.exports = 'main-module' })
那么調(diào)用這個(gè)factory的時(shí)候,exports就為module.exports,也是是字符串"main-moudle"
。最后callback傳入的參數(shù)就是"main-moudle"
。所以我們執(zhí)行最開(kāi)頭寫(xiě)的那段代碼,最后會(huì)在頁(yè)面上彈出main-moudle
。
define定義模塊
你以為到這里就結(jié)束了嗎?并沒(méi)有。前面只說(shuō)了加載依賴(lài)模塊中define方法中沒(méi)有其他依賴(lài),那如果有其他依賴(lài)呢?廢話(huà)不多說(shuō),先看看define方法做了什么:
global.define = Module.define Module.define = function (id, deps, factory) { var argsLen = arguments.length // 參數(shù)校準(zhǔn) if (argsLen === 1) { factory = id id = undefined } else if (argsLen === 2) { factory = deps if (isArray(id)) { deps = id id = undefined } else { deps = undefined } } // 如果沒(méi)有直接傳入依賴(lài)數(shù)組 // 則從factory中提取所有的依賴(lài)模塊到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事件進(jìn)行save anonymousMeta = meta }
首先進(jìn)行了參數(shù)的修正,這個(gè)邏輯很簡(jiǎn)單,直接跳過(guò)。第二步判斷了有沒(méi)有依賴(lài)數(shù)組,如果沒(méi)有,就通過(guò)parseDependencies方法從factory中獲取。這個(gè)方法很有意思,是一個(gè)狀態(tài)機(jī),會(huì)一步步的去解析字符串,匹配到require,將其中的模塊取出,最后放到一個(gè)數(shù)組里。這個(gè)方法在requirejs中是通過(guò)正則實(shí)現(xiàn)的,早期seajs也是通過(guò)正則匹配的,后來(lái)改成了這種狀態(tài)機(jī)的方式,可能是考慮到性能的問(wèn)題。seajs的倉(cāng)庫(kù)中專(zhuān)門(mén)有一個(gè)模塊來(lái)講這個(gè)東西的,請(qǐng)看鏈接。
獲取到依賴(lài)模塊之后又設(shè)置了一個(gè)meta對(duì)象,這個(gè)就表示這個(gè)模塊的原數(shù)據(jù),里面有記錄模塊的依賴(lài)項(xiàng)、id、factory等。如果這個(gè)模塊define的時(shí)候沒(méi)有設(shè)置id,就表示是個(gè)匿名模塊,那怎么才能與之前發(fā)起請(qǐng)求的那個(gè)mod相匹配呢?
這里就有了一個(gè)全局變量anonymousMeta
,先將元數(shù)據(jù)放入這個(gè)對(duì)象。然后回過(guò)頭看看模塊加載時(shí)設(shè)置的onload函數(shù)里面有一段就是獲取這個(gè)全局變量的。
function onRequest(error) { //模塊加載完畢的回調(diào) ... // 保存元數(shù)據(jù)到匿名模塊,uri為請(qǐng)求js的uri if (anonymousMeta) { Module.save(uri, anonymousMeta) anonymousMeta = null } ... }
不管是不是匿名模塊,最后都是通過(guò)save方法,將元數(shù)據(jù)存入到mod中。
// 存儲(chǔ)元數(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 } }
這里完成之后,就是和前面的邏輯一樣了,先去校驗(yàn)當(dāng)前模塊有沒(méi)有依賴(lài)項(xiàng),如果有依賴(lài)項(xiàng),就去加載依賴(lài)項(xiàng)和use的邏輯是一樣的,等依賴(lài)項(xiàng)全部加載完畢后,通知入口模塊的remain減1,知道remain為0,最后調(diào)用入口模塊的回調(diào)方法。整個(gè)seajs的邏輯就已經(jīng)全部走通,Yeah!
結(jié)語(yǔ)
有過(guò)看requirejs的經(jīng)驗(yàn),再來(lái)看seajs還是順暢很多,對(duì)模塊化的理解有了更加深刻的理解。閱讀源碼之前還是得對(duì)框架有個(gè)基本認(rèn)識(shí),并且有使用過(guò),要不然很多地方都很懵懂。所以以后還是閱讀一些工作中有經(jīng)常使用的框架或類(lèi)庫(kù)的源碼進(jìn)行閱讀,不能總像個(gè)無(wú)頭蒼蠅一樣。
最后用一張流程圖,總結(jié)下seajs的加載過(guò)程。