這裡是seajs loader的核心部分,有些IE相容的部分還不是很明白,主要是理解各個模組如何依賴有序載入,以及CMD規範。
代碼有點長,需要耐心看:
複製代碼 代碼如下:
/**
* The core of loader
*/
;(function(seajs, util, config) {
// 模組緩衝
var cachedModules = {}
// 介面修改緩衝
var cachedModifiers = {}
// 編譯隊列
var compileStack = []
// 模組狀態
var STATUS = {
'FETCHING': 1, // The module file is fetching now. 模組正在下載中
'FETCHED': 2, // The module file has been fetched. 模組已下載
'SAVED': 3, // The module info has been saved. 模組資訊已儲存
'READY': 4, // All dependencies and self are ready to compile. 模組的依賴項都已下載,等待編譯
'COMPILING': 5, // The module is in compiling now. 模組正在編譯中
'COMPILED': 6 // The module is compiled and module.exports is available. 模組已編譯
}
function Module(uri, status) {
this.uri = uri
this.status = status || 0
// this.id is set when saving
// this.dependencies is set when saving
// this.factory is set when saving
// this.exports is set when compiling
// this.parent is set when compiling
// this.require is set when compiling
}
Module.prototype._use = function(ids, callback) {
//轉換為數組,統一操作
util.isString(ids) && (ids = [ids])
// 使用模組系統內部的路徑解析機制來解析並返回模組路徑
var uris = resolve(ids, this.uri)
this._load(uris, function() {
// Loads preload files introduced in modules before compiling.
// 在編譯之前,再次調用preload預先載入模組
// 因為在代碼執行期間,隨時可以調用seajs.config配置預先載入模組
preload(function() {
// 編譯每個模組,並將各個模組的exports作為參數傳遞給回呼函數
var args = util.map(uris, function(uri) {
return uri ? cachedModules[uri]._compile() : null
})
if (callback) {
// null使回呼函數中this指標為window
callback.apply(null, args)
}
})
})
}
// 主模組載入相依模組(稱之為子模組),並執行回呼函數
Module.prototype._load = function(uris, callback) {
// 過濾uris數組
// 情況一:緩衝中不存在該模組,返回其uri
// 情況二:緩衝中存在該模組,但是其status < STATUS.READY(即還沒準備好編譯)
var unLoadedUris = util.filter(uris, function(uri) {
return uri && (!cachedModules[uri] ||
cachedModules[uri].status < STATUS.READY)
})
var length = unLoadedUris.length
// 如果length為0,表示依賴項為0或者都已下載完成,那麼執行回調編譯操作
if (length === 0) {
callback()
return
}
var remain = length
for (var i = 0; i < length; i++) {
// 閉包,為onFetched函數提供上下文環境
(function(uri) {
// 建立模組對象
var module = cachedModules[uri] ||
(cachedModules[uri] = new Module(uri, STATUS.FETCHING))
//如果模組已下載,那麼執行onFetched,否則執行fetch操作(請求模組)
module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched)
function onFetched() {
// cachedModules[uri] is changed in un-correspondence case
module = cachedModules[uri]
// 如果模組狀態為SAVED,表示模組的依賴項已經確定,那麼下載相依模組
if (module.status >= STATUS.SAVED) {
// 從模組資訊中擷取相依模組列表,並作循環相依性的處理
var deps = getPureDependencies(module)
// 如果存在依賴項,繼續下載
if (deps.length) {
Module.prototype._load(deps, function() {
cb(module)
})
}
// 否則直接執行cb
else {
cb(module)
}
}
// Maybe failed to fetch successfully, such as 404 or non-module.
// In these cases, just call cb function directly.
// 如果下載模組不成功,比如404或者模組不規範(代碼出錯),導致此時模組狀態可能為fetching,或者fetched
// 此時直接執行回呼函數,在編譯模組時,該模組就只會返回null
else {
cb()
}
}
})(unLoadedUris[i])
}
function cb(module) {
// 更改模組狀態為READY,當remain為0時表示模組依賴都已經下完,那麼執行callback
(module || {}).status < STATUS.READY && (module.status = STATUS.READY)
--remain === 0 && callback()
}
}
Module.prototype._compile = function() {
var module = this
// 如果該模組已經編譯過,則直接返回module.exports
if (module.status === STATUS.COMPILED) {
return module.exports
}
// Just return null when:
// 1. the module file is 404.
// 2. the module file is not written with valid module format.
// 3. other error cases.
// 這裡是處理一些異常情況,此時直接返回null
if (module.status < STATUS.SAVED && !hasModifiers(module)) {
return null
}
// 更改模組狀態為COMPILING,表示模組正在編譯
module.status = STATUS.COMPILING
// 模組內部使用,是一個方法,用來擷取其他模組提供(稱之為子模組)的介面,同步操作
function require(id) {
// 根據id解析模組的路徑
var uri = resolve(id, module.uri)
// 從模組緩衝中擷取模組(注意,其實這裡子模組作為主模組的依賴項是已經被下載下來的)
var child = cachedModules[uri]
// Just return null when uri is invalid.
// 如果child為空白,只能表示參數填寫出錯導致uri不正確,那麼直接返回null
if (!child) {
return null
}
// Avoids circular calls.
// 如果子模組的狀態為STATUS.COMPILING,直接返回child.exports,避免因為循環相依性反覆編譯模組
if (child.status === STATUS.COMPILING) {
return child.exports
}
// 指向初始化時調用當前模組的模組。根據該屬性,可以得到模組初始化時的Call Stack.
child.parent = module
// 返回編譯過的child的module.exports
return child._compile()
}
// 模組內部使用,用來非同步載入模組,並在載入完成後執行指定回調。
require.async = function(ids, callback) {
module._use(ids, callback)
}
// 使用模組系統內部的路徑解析機制來解析並返回模組路徑。該函數不會載入模組,只返回解析後的絕對路徑。
require.resolve = function(id) {
return resolve(id, module.uri)
}
// 通過該屬性,可以查看到模組系統載入過的所有模組。
// 在某些情況下,如果需要重新載入某個模組,可以得到該模組的 uri, 然後通過 delete require.cache[uri] 來將其資訊刪除掉。這樣下次使用時,就會重新擷取。
require.cache = cachedModules
// require是一個方法,用來擷取其他模組提供的介面。
module.require = require
// exports是一個對象,用來向外提供模組介面。
module.exports = {}
var factory = module.factory
// factory 為函數時,表示模組的構造方法。執行該方法,可以得到模組向外提供的介面。
if (util.isFunction(factory)) {
compileStack.push(module)
runInModuleContext(factory, module)
compileStack.pop()
}
// factory 為對象、字串等非函數類型時,表示模組的介面就是該對象、字串等值。
// 如:define({ "foo": "bar" });
// 如:define('I am a template. My name is {{name}}.');
else if (factory !== undefined) {
module.exports = factory
}
// 更改模組狀態為COMPILED,表示模組已編譯
module.status = STATUS.COMPILED
// 執行模組介面修改,通過seajs.modify()
execModifiers(module)
return module.exports
}
Module._define = function(id, deps, factory) {
var argsLength = arguments.length
// 根據傳入的參數個數,進行參數匹配
// define(factory)
// 一個參數的情況:
// id : undefined
// deps : undefined(後面會根據正則取出相依模組列表)
// factory : function
if (argsLength === 1) {
factory = id
id = undefined
}
// define(id || deps, factory)
// 兩個參數的情況:
else if (argsLength === 2) {
// 預設情況下 :define(id, factory)
// id : '...'
// deps : undefined
// factory : function
factory = deps
deps = undefined
// define(deps, factory)
// 如果第一個參數為數組 :define(deps, factory)
// id : undefined
// deps : [...]
// factory : function
if (util.isArray(id)) {
deps = id
id = undefined
}
}
// Parses dependencies.
// 如果deps不是數組(即deps未指定值),那麼通過Regex解析依賴
if (!util.isArray(deps) && util.isFunction(factory)) {
deps = util.parseDependencies(factory.toString())
}
// 元資訊,之後會將資訊傳遞給對應的module對象中
var meta = { id: id, dependencies: deps, factory: factory }
var derivedUri
// Try to derive uri in IE6-9 for anonymous modules.
// 對於IE6-9,嘗試通過interactive script擷取模組的uri
if (document.attachEvent) {
// Try to get the current script.
// 擷取當前的script
var script = util.getCurrentScript()
if (script) {
// 將當前script的url進行unpareseMap操作,與模組緩衝中key保持一致
derivedUri = util.unParseMap(util.getScriptAbsoluteSrc(script))
}
if (!derivedUri) {
util.log('Failed to derive URI from interactive script for:',
factory.toString(), 'warn')
// NOTE: If the id-deriving methods above is failed, then falls back
// to use onload event to get the uri.
}
}
// Gets uri directly for specific module.
// 如果給定id,那麼根據id解析路徑
// 顯然如果沒指定id:
// 對於非IE瀏覽器而言,則返回undefined(derivedUri為空白)
// 對於IE瀏覽器則返回CurrentScript的src
// 如果指定id:
// 則均返回有seajs解析(resolve)過的路徑url
var resolvedUri = id ? resolve(id) : derivedUri
// uri存在的情況,進行模組資訊儲存
if (resolvedUri) {
// For IE:
// If the first module in a package is not the cachedModules[derivedUri]
// self, it should assign to the correct module when found.
if (resolvedUri === derivedUri) {
var refModule = cachedModules[derivedUri]
if (refModule && refModule.realUri &&
refModule.status === STATUS.SAVED) {
cachedModules[derivedUri] = null
}
}
// 儲存模組資訊
var module = save(resolvedUri, meta)
// For IE:
// Assigns the first module in package to cachedModules[derivedUrl]
if (derivedUri) {
// cachedModules[derivedUri] may be undefined in combo case.
if ((cachedModules[derivedUri] || {}).status === STATUS.FETCHING) {
cachedModules[derivedUri] = module
module.realUri = derivedUri
}
}
else {
// 將第一個模組儲存到firstModuleInPackage
firstModuleInPackage || (firstModuleInPackage = module)
}
}
// uri不存在的情況,在onload回調中進行模組資訊儲存,那裡有個閉包
else {
// Saves information for "memoizing" work in the onload event.
// 因為此時的uri不知道,所以將元資訊暫時儲存在anonymousModuleMeta中,在onload回調中進行模組save操作
anonymousModuleMeta = meta
}
}
// 擷取正在編譯的模組
Module._getCompilingModule = function() {
return compileStack[compileStack.length - 1]
}
// 從seajs.cache中快速查看和擷取已載入的模組介面,傳回值是module.exports數組
// selector 支援字串和Regex
Module._find = function(selector) {
var matches = []
util.forEach(util.keys(cachedModules), function(uri) {
if (util.isString(selector) && uri.indexOf(selector) > -1 ||
util.isRegExp(selector) && selector.test(uri)) {
var module = cachedModules[uri]
module.exports && matches.push(module.exports)
}
})
return matches
}
// 修改模組介面
Module._modify = function(id, modifier) {
var uri = resolve(id)
var module = cachedModules[uri]
// 如果模組存在,並且處於COMPILED狀態,那麼執行修改介面操作
if (module && module.status === STATUS.COMPILED) {
runInModuleContext(modifier, module)
}
// 否則放入修改介面緩衝中
else {
cachedModifiers[uri] || (cachedModifiers[uri] = [])
cachedModifiers[uri].push(modifier)
}
return seajs
}
// For plugin developers
Module.STATUS = STATUS
Module._resolve = util.id2Uri
Module._fetch = util.fetch
Module.cache = cachedModules
// Helpers
// -------
// 正在下載的模組列表
var fetchingList = {}
// 已下載的模組列表
var fetchedList = {}
// 回呼函數列表
var callbackList = {}
// 匿名模組元資訊
var anonymousModuleMeta = null
var firstModuleInPackage = null
// 循環相依性棧
var circularCheckStack = []
// 批量解析模組的路徑
function resolve(ids, refUri) {
if (util.isString(ids)) {
return Module._resolve(ids, refUri)
}
return util.map(ids, function(id) {
return resolve(id, refUri)
})
}
function fetch(uri, callback) {
// fetch時,首先將uri按map規則轉換
var requestUri = util.parseMap(uri)
// 在fethedList(已下載的模組列表)中尋找,有的話,直接返回,並執行回呼函數
// TODO : 為什麼這一步,fetchedList可能會存在該模?
if (fetchedList[requestUri]) {
// See test/issues/debug-using-map
cachedModules[uri] = cachedModules[requestUri]
callback()
return
}
// 在fetchingList(正在在下載的模組列表)中尋找,有的話,只需添加回呼函數到列表中去,然後直接返回
if (fetchingList[requestUri]) {
callbackList[requestUri].push(callback)
return
}
// 如果走到這一步,表示該模組是第一次被請求,
// 那麼在fetchingList插入該模組的資訊,表示該模組已經處於下載列表中,並初始化該模組對應的回呼函數列表
fetchingList[requestUri] = true
callbackList[requestUri] = [callback]
// Fetches it
// 擷取該模組,即發起請求
Module._fetch(
requestUri,
function() {
// 在fetchedList插入該模組的資訊,表示該模組已經下載完成
fetchedList[requestUri] = true
// Updates module status
var module = cachedModules[uri]
// 此時status可能為STATUS.SAVED,之前在_define中已經說過
if (module.status === STATUS.FETCHING) {
module.status = STATUS.FETCHED
}
// Saves anonymous module meta data
// 因為是匿名模組(此時通過閉包擷取到uri,在這裡儲存模組資訊)
// 並將anonymousModuleMeta置為空白
if (anonymousModuleMeta) {
save(uri, anonymousModuleMeta)
anonymousModuleMeta = null
}
// Assigns the first module in package to cachedModules[uri]
// See: test/issues/un-correspondence
if (firstModuleInPackage && module.status === STATUS.FETCHED) {
cachedModules[uri] = firstModuleInPackage
firstModuleInPackage.realUri = uri
}
firstModuleInPackage = null
// Clears
// 在fetchingList清除模組資訊,因為已經該模組fetched並save
if (fetchingList[requestUri]) {
delete fetchingList[requestUri]
}
// Calls callbackList
// 依次調用回呼函數,並清除回呼函數列表
if (callbackList[requestUri]) {
util.forEach(callbackList[requestUri], function(fn) {
fn()
})
delete callbackList[requestUri]
}
},
config.charset
)
}
function save(uri, meta) {
var module = cachedModules[uri] || (cachedModules[uri] = new Module(uri))
// Don't override already saved module
// 此時status可能有兩個狀態:
// STATUS.FETCHING,在define裡面調用(指定了id),儲存模組資訊
// STATUS.FETCHED,在onload的回呼函數裡調用,儲存模組資訊
if (module.status < STATUS.SAVED) {
// Lets anonymous module id equal to its uri
// 匿名模組(即沒有指定id),用它的uri作為id
module.id = meta.id || uri
// 將依賴項(數組)解析成的絕對路徑,儲存到模組資訊中
module.dependencies = resolve(
util.filter(meta.dependencies || [], function(dep) {
return !!dep
}), uri)
// 儲存factory(要執行的模組代碼,也可能是對象或者字串等)
module.factory = meta.factory
// Updates module status
// 更新模組狀態為SAVED,(注意此時它只是擁有了依賴項,還未全部下載下來(即還未READY))
module.status = STATUS.SAVED
}
return module
}
// 根據模組上下文執行模組代碼
function runInModuleContext(fn, module) {
// 傳入與模組相關的兩個參數以及模組自身
// exports用來暴露介面
// require用來擷取相依模組(同步)(編譯)
var ret = fn(module.require, module.exports, module)
// 支援傳回值暴露介面形式,如:
// return {
// fn1 : xx
// ,fn2 : xx
// ...
// }
if (ret !== undefined) {
module.exports = ret
}
}
// 判斷模組是否存在介面修改
function hasModifiers(module) {
return !!cachedModifiers[module.realUri || module.uri]
}
// 修改模組介面
function execModifiers(module) {
var uri = module.realUri || module.uri
var modifiers = cachedModifiers[uri]
// 內部變數 cachedModifiers 就是用來儲存使用者通過 seajs.modify 方法定義的修改點
// 查看該uri是否又被modify更改過
if (modifiers) {
// 對修改點統一執行factory,返回修改後的module.exports
util.forEach(modifiers, function(modifier) {
runInModuleContext(modifier, module)
})
// 刪除 modify 方法定義的修改點 ,避免再次執行
delete cachedModifiers[uri]
}
}
//擷取純粹的依賴關係,得到不存在循環相依性關係的依賴數組
function getPureDependencies(module) {
var uri = module.uri
// 對每個依賴項進行過濾,對於有可能形成循環相依性的進行剔除,並列印出警告日誌
return util.filter(module.dependencies, function(dep) {
// 首先將被檢查模組的uri放到循環相依性檢查棧中,之後的檢查會用到
circularCheckStack = [uri]
//接下來檢查模組uri是否和其依賴的模組存在循環相依性
var isCircular = isCircularWaiting(cachedModules[dep])
if (isCircular) {
// 如果迴圈,則將uri放到循環相依性檢查棧中
circularCheckStack.push(uri)
// 列印出迴圈警告日誌
printCircularLog(circularCheckStack)
}
return !isCircular
})
}
function isCircularWaiting(module) {
// 如果相依模組不存在,那麼返回false,因為此時也無法獲得相依模組的依賴項,所以這裡無法做判斷
// 或者如果模組的狀態值等於saved,也返回false,因為模組狀態為saved的時候代表該模組的資訊已經有了,
// 所以儘管形成了循環相依性,但是require主模組時,同樣可以正常編譯,返回主模組介面(好像nodejs會返回undefined)
if (!module || module.status !== STATUS.SAVED) {
return false
}
// 如果不是以上的情況,那麼將相依模組的uri放到循環相依性檢查棧中,之後的檢查會用到
circularCheckStack.push(module.uri)
// 再次取相依模組的相依模組
var deps = module.dependencies
if (deps.length) {
// 通過循環相依性檢查棧,檢查是否存在循環相依性(這裡是第一層相依模組檢查,與主模組循環相依性的情況)
if (isOverlap(deps, circularCheckStack)) {
return true
}
// 如果不存在上述情形,那麼進一步查看,相依模組的相依模組,查看他們是否存在對循環相依性檢查棧中的uri的模組存在循環相依性
// 這樣的話,就遞迴了,循環相依性檢查棧就像形成的一條鏈,當前模組依次對主模組,主模組的主模組...直到最頂上的主模組,依次進行判斷是否存在依賴
for (var i = 0; i < deps.length; i++) {
if (isCircularWaiting(cachedModules[deps[i]])) {
return true
}
}
}
// 如果不存在循環相依性,那麼pop出之前已經push進的模組uri,並返回false
circularCheckStack.pop()
return false
}
// 列印出迴圈警告日誌
function printCircularLog(stack, type) {
util.log('Found circular dependencies:', stack.join(' --> '), type)
}
//判斷兩個數組是否有重複的值
function isOverlap(arrA, arrB) {
var arrC = arrA.concat(arrB)
return arrC.length > util.unique(arrC).length
}
// 從設定檔讀取是否有需要提前載入的模組
// 如果有積極式載入模組,首先設定預先載入模組為空白(保證下次不必重複載入),並載入預先載入模組並執行回調,如果沒有則順序執行
function preload(callback) {
var preloadMods = config.preload.slice()
config.preload = []
preloadMods.length ? globalModule._use(preloadMods, callback) : callback()
}
// Public API
// 對外暴露的API
// ----------
// 全域模組,可以認為是頁面模組,頁面中的js,css檔案都是通過它來載入的
// 模組初始狀態就是COMPILED,uri就是頁面的uri
var globalModule = new Module(util.pageUri, STATUS.COMPILED)
// 頁面js,css檔案載入器
seajs.use = function(ids, callback) {
// Loads preload modules before all other modules.
// 預先載入模組
preload(function() {
globalModule._use(ids, callback)
})
// Chain
return seajs
}
// For normal users
// 供普通使用者調用
seajs.define = Module._define
seajs.cache = Module.cache
seajs.find = Module._find
seajs.modify = Module._modify
// For plugin developers
// 供開發人員使用
seajs.pluginSDK = {
Module: Module,
util: util,
config: config
}
})(seajs, seajs._util, seajs._config)