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