寫筆記的時候才注意到我看的源代碼是 3.0.0 的,可是官方發佈的最新版本是 2.3.0。相信大部分是相同的,因此先把這個記完,再看一次 2.3.0 的代碼。jquery
seajs 的源代碼能夠在 github上獲取。seajs 在文檔「如何參與開發」中說明了閱讀順序,固然爲了便於閱讀,在瞭解了目錄結構以後,我直接閱讀了合併好的 sea-debug.js。git
整個seajs採用的是2空格縮進,避免分號的寫法,我不是很習慣,但不影響閱讀。github
文檔中各個源文件所包含的內容大體以下:緩存
intro.js
文件頭ide
sea.js
定義全局global.seajs
對象和seajs.data
對象函數
util-lang.js
,語言相關工具
定義用於判斷對象類型的isXxxxx()
函數,以及一個與語言無關的cid()
。工具
util-events.js
定義 seajs 的事件處理相關函數,包括on()
、off()
、emit()
fetch
util-path.js
定義用於路徑處理的工具函數this
util-request.js
定義請求文件的工具函數seajs.request()
等spa
util-deps.js
定義用於分析依賴關係的parseDependencies()
module.js
seajs 的核心,模塊類。
也包含部分 seajs 的方法
config.js
定義seajs.config()
,以及data
部分屬性的默認值
因此合併以後的整個 seajs 代碼看起來就像這樣
(function(global, undefiend) { global.seajs = { data: {} } var isObject = function() {} var isString = function() {} var isArray = function() {} var isFunction = function() {} var cid = function() {} data.events = {} seajs.on = function() {} seajs.off = function() {} seajs.emit = function() {} // path utils, and seajs.resolve = function() {} var loaderDir var parseDependencies = function() {} function Module() {} seajs.config = function() {} })(this);
首先是定義了一個產生 isXxxx 的函數工廠 isType()
function isType(type) { return function(obj) { return {}.toString.call(obj) == "[object " + type + "]" } }
從這個工廠的代碼能夠看出來,isXxxx()
主要是經過 Object.prototype.toString
的值來判斷對象類型的。
固然也有例外:
// 畢竟 Array.isArray() 是 [native code],效果會高得多 var isArray = Array.isArray || isType("Array")
這裏有幾件事情我不是很明白:
就是爲何不使用
typeof
運算符來判斷類型,通常語言中運算符實現會比比較字符串快得多。
{}.toString.call(obj)
和Object.prototype.toString.call(obj)
的做用是同樣的,可是在運行時,每執行一次isXxxx
就會產生一個新的{}
對象;而Object.prototype
始終都是同一個對象,彷佛能夠減小很多開銷jQuery 的
isFunction()
等方法都是經過jQuery.type()
來實現的,而jQuery.type
中則是經過定義了一個class2type
字典對象來作類型映射,
2014-09-09 補充
我在 seajs 的 issue 上問了這個問題,因而上面的幾個問題就都找到答案了:
https://github.com/seajs/seajs/issues/1314
參考玉伯的博客
https://github.com/lifesinger/lifesinger.github.com/issues/175
jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); });
這種做法也會比每次都拼字符串(
"[object " + type + "]"
)來比較要效率高一些吧。不過 seajs 和 jquery 都沒有直接依賴
typeof
運算符來實現isXxxx
,我相信絕對不是偶然,必定有啥緣由,不過這個緣由我目前就不清楚了,但願玉伯能看到這個博客,稍做解釋
seajs 只爲全局對象 seajs
添加了事件處理。事件的回調函數鏈保存在 seajs.data.events
中,以事件名稱爲 key,Array
對象保存的回調函數鏈爲 value。
由於 seajs 的事件主要爲了插件而定義,因此對參數並無嚴格的校驗。好比
// 自定義一個比較奇葩的事件,這不會報錯 seajs.on(null, "fake callback"); // 可是執行就會出錯了 seajs.emit("null") // 輸出:TypeError: string is not a function
看樣子,插件開發者得本身注意下這個問題了。
看完 seajs 的源碼,大概定義了這麼一些事件
error
,貌似跟 NodeJS 有點關係,沒仔細看
load
,在模塊對象狀態變成 LOADING
後觸發,參數是全部依賴模塊的 URI。
// Emit `load` event for plugins such as combo plugin var uris = mod.resolve() emit("load", uris)
exec
,在模塊對象狀態變成 EXECUTED
後觸發,參數就是模塊對象自己
fetch
,在模塊對象狀態剛變成 FETECHING
時觸發,參數是一個臨時對象emitData
,事件結果保存在 emitData.requestUri
,用於後面的 request
請求數據。
// Emit `fetch` event for plugins such as combo plugin var emitData = { uri: uri } emit("fetch", emitData) var requestUri = emitData.requestUri || uri
request
,在 fetch
事件後對 emitData.requestUri || uri
進行了處理以後,經過 seajs.request()
請求數據以前觸發,參數是一個臨時對象,變量名複用的 emitData
。事件處理完成後根據 emitData.requested
的值來判斷是否須要調用 seajs.request
請求數據。
// Emit `request` event for plugins such as text plugin emit("request", emitData = { uri: uri, requestUri: requestUri, onRequest: onRequest, charset: isFunction(data.charset) ? data.charset(requestUri) || 'utf-8' : data.charset }) if (!emitData.requested) { // ... }
resolve
,在 Module.resolve
中調用 seajs.resolve()
以前觸發,參數是一個臨時對象 emitData
。事件中若是產生了有效的 emitData.uri
,則再也不調用seajs.resolve()
// Emit `resolve` event for plugins such as text plugin var emitData = { id: id, refUri: refUri } emit("resolve", emitData) return emitData.uri || seajs.resolve(emitData.id, refUri)
config
,在 seajs.config()
中,完成對 config 對象的處理以後觸發,參數就是 config 對象。
seajs.config = function(configData) { // ... emit("config", configData) return seajs }
Module
類纔是 seajs 的重頭戲,核心的核心。seajs 做爲一個模塊加載器,因此模塊都是以一個 Module
對象保存在 cachedMods
中的。
var cachedMods = seajs.cache = {}
每一個模塊都有 8 種狀態,它必定是在這 8 種狀態之一,並且貌似狀態改變仍是不可逆的。
var STATUS = Module.STATUS = { // 沒定義狀態值爲 0 的狀態常量,這是初始狀態 // 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, // 7 - 404 ERROR: 7 }
其中 SAVED
狀態能夠理解爲 FETCHED
狀態。除了初始狀態 0
和錯誤狀態 ERROR: 7
以外,其它 6 個狀態都是成對出現的,即 ING
狀態和 ED
狀態,這三對狀態清晰的劃分出來三個處理過程:fetch、load、exec,對應於模塊對象的 3 個方法:fetch()
, load()
, exec()
。
從代碼內容來看,這三個主要過程方法主要功能分別能夠用一句話說明:
fetch
,從 URL 加載模塊定義,獲得 factory 函數,並將 factory 函數賦值給對應的模塊對象(經過 Module.get()
建立或獲取);
load
,fetch 並 load 全部依賴模塊,並在保證全部依賴模塊都是 LOADED 狀態以後,調用入口模塊(_entry
)的 callback
(貌似只有經過 seajs.use()
建立的匿名模塊纔有 callback
)
exec
,在調用這個方法的時候,能夠保證全部依賴模塊都已是 LOADED 狀態了,因此 exec
就只是簡單的執行 factory 函數,並返回 exports
。factory 只執行一次,而後將 exports
緩存下來。
如今來看看入口函數 seajs.use()
、模塊定義函數 define()
、模塊關係過程處理方法 fetch()
,load()
, exec()
的主要調用關係:
// seajs.use 只調用了 Module.use,因此它們的調用關係能夠看做等同 seajs.use = Module.use = function() { Module.get() exec() // 經過 _entry.callback 調用 load() }
define = Module.define = function() { Module.save(id, factory) }
Module.prototype.fetch = function() { define() // 經過 seajs.request() 調用 load() }
Module.prototype.load = function() { pass() fetch() // [遞歸] // 結束的條件是 _entry.remain === 1, // 即當前是最後一個依賴模塊 // 遞歸結束時調用 _entry.callback,即調用了 exec load() }
Module.prototype.exec = function() { exec() // 經過 define 中定義的 factory 函數調用 }
Module.prototype.pass = function() { // [遞歸] // 結束條件是把 _entry 傳遞到最末一層依賴 // 遞歸過程經過 _entry.remain 進行了引用記數 pass() }
各函數和方法具體處理過程看代碼就明白了,由於遞歸關係有點複雜,還有一些回調關係在裏面,因此看起來有點繞,不過還算是看得明白。
http://seajs.org 引用的 seajs 版本是 2.2.1,從這引頁面的控制輸出 seajs.Modules.prototype
來看,並無定義 pass()
方法,因此對 _entry 的處理可能會有點不同,稍後看了 seajs 2.2.3 版本的代碼就知道了。