近幾年前端工程化越來越完善,打包工具也已經(jīng)是前端標配了,像seajs這種老古董早已停止維護,而且使用的人估計也幾個了。但這并不能阻止好奇的我,為了了解當年的前端前輩們是如何在瀏覽器進行代碼模塊化的,我鼓起勇氣翻開了Seajs的源碼。下面就和我一起細細品味Seajs源碼吧。
創(chuàng)新互聯(lián)基于成都重慶香港及美國等地區(qū)分布式IDC機房數(shù)據(jù)中心構建的電信大帶寬,聯(lián)通大帶寬,移動大帶寬,多線BGP大帶寬租用,是為眾多客戶提供專業(yè)成都天府聯(lián)通服務器托管報價,主機托管價格性價比高,為金融證券行業(yè)服務器托管,ai人工智能服務器托管提供bgp線路100M獨享,G口帶寬及機柜租用的專業(yè)成都idc公司。
如何使用seajs
在看Seajs源碼之前,先看看Seajs是如何使用的,畢竟剛?cè)胄械臅r候,大家就都使用browserify、webpack之類的東西了,還從來沒有用過Seajs。
<!-- 首先在頁面中引入sea.js,也可以使用cdn資源 -->
<script type="text/javascript" src="./sea.js"></script>
<script>
// 設置一些參數(shù)
seajs.config({
debug: true, // debug為false時,在模塊加載完畢后會移除head中的script標簽
base: './js/', // 通過路徑加載其他模塊的默認根目錄
alias: { // 別名
jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery'
}
})
seajs.use('main', function(main) {
alert(main)
})
</script>
//main.js
define(function (require, exports, module) {
// require('jquery')
// var $ = window.$
module.exports = 'main-module'
})seajs的參數(shù)配置
首先通過script導入seajs,然后對seajs進行一些配置。seajs的配置參數(shù)很多具體不詳細介紹,seajs將配置項會存入一個私有對象data中,并且如果之前有設置過某個屬性,并且這個屬性是數(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] // 獲取當前配置
var prev = data[key] // 獲取之前的配置
if (prev && isObject(prev)) { // 如果之前已經(jīng)設置過,且為一個對象
for (var k in curr) {
prev[k] = curr[k] // 用新值覆蓋舊值,舊值保留不變
}
}
else {
// 如果之前的值為數(shù)組,進行concat
if (isArray(prev)) {
curr = prev.concat(curr)
}
// 確保 base 為一個路徑
else if (key === "base") {
// 必須已 "/" 結尾
if (curr.slice(-1) !== "/") {
curr += "/"
}
curr = addBase(curr) // 轉(zhuǎn)換為絕對路徑
}
// Set config
data[key] = curr
}
}
}
})(this);設置的時候還有個比較特殊的地方,就是base這個屬性。這表示所有模塊加載的基礎路徑,所以格式必須為一個路徑,并且該路徑最后會轉(zhuǎn)換為絕對路徑。比如,我的配置為base: './js',我當前訪問的域名為http://qq.com/web/index.html,最后base屬性會被轉(zhuǎn)化為http://qq.com/web/js/。然后,所有依賴的模塊id都會根據(jù)該路徑轉(zhuǎn)換為uri,除非有定義其他配置,關于配置點到為止,到用到的地方再來細說。
模塊的加載與執(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為一個構造函數(shù),里面掛載了所有與模塊加載相關的方法,還有很多靜態(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) // 表示當前模塊的入口為本身,后面還會把這個值傳入他的依賴模塊
mod.history = {}
mod.remain = 1 // 這個值后面會用來標識依賴模塊是否已經(jīng)全部加載完畢
mod.callback = function() { //設置模塊加載完畢的回調(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方法一共做了三件事:
1.調(diào)用Module.get,進行Module實例化
2.為模塊綁定回調(diào)函數(shù)
3.調(diào)用load,進行依賴模塊的加載
實例化模塊,一切的開端
首先use方法調(diào)用了get靜態(tài)方法,這個方法是對Module進行實例化,并且將實例化的對象存入到全局對象cachedMods中進行緩存,并且以uri作為模塊的標識,如果之后有其他模塊加載該模塊就能直接在緩存中獲取。
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",會在前面加上我們之前設置的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方法的時候設置的
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替換為別名對應的地址
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 // 在入口模塊標識曾經(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 = { // 設置加載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標簽,綁定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方法中給入口模塊設置callback方法嗎,沒錯,兜兜轉(zhuǎn)轉(zhuǎn)我們又回到了起點。
mod.callback = function() { //設置模塊加載完畢的回調(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相關三個參數(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定義模塊
你以為到這里就結束了嗎?并沒有。前面只說了加載依賴模塊中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的倉庫中專門有一個模塊來講這個東西的,請看鏈接。
獲取到依賴模塊之后又設置了一個meta對象,這個就表示這個模塊的原數(shù)據(jù),里面有記錄模塊的依賴項、id、factory等。如果這個模塊define的時候沒有設置id,就表示是個匿名模塊,那怎么才能與之前發(fā)起請求的那個mod相匹配呢?
這里就有了一個全局變量anonymousMeta,先將元數(shù)據(jù)放入這個對象。然后回過頭看看模塊加載時設置的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
}
}這里完成之后,就是和前面的邏輯一樣了,先去校驗當前模塊有沒有依賴項,如果有依賴項,就去加載依賴項和use的邏輯是一樣的,等依賴項全部加載完畢后,通知入口模塊的remain減1,知道remain為0,最后調(diào)用入口模塊的回調(diào)方法。整個seajs的邏輯就已經(jīng)全部走通,Yeah!
結語
有過看requirejs的經(jīng)驗,再來看seajs還是順暢很多,對模塊化的理解有了更加深刻的理解。閱讀源碼之前還是得對框架有個基本認識,并且有使用過,要不然很多地方都很懵懂。所以以后還是閱讀一些工作中有經(jīng)常使用的框架或類庫的源碼進行閱讀,不能總像個無頭蒼蠅一樣。
最后用一張流程圖,總結下seajs的加載過程。

分享文章:Seajs源碼詳解分析
URL分享:http://chinadenli.net/article12/gechdc.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供做網(wǎng)站、面包屑導航、網(wǎng)站收錄、云服務器、動態(tài)網(wǎng)站、靜態(tài)網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)