1,seajs的主要内容在module.js内部,最开始包含这么几个东西
var cachedMods = seajs.cache = {} var anonymousMeta var fetchingList = {} var fetchedList = {} var callbackList = {}
还有几个状态:
var STATUS = Module.STATUS = { // 1 - `module.uri` 获取中 FETCHING: 1, // 2 - 模块已经存储在了cachedMods对象中 SAVED: 2, // 3 - 模块依赖加载中 LOADING: 3, // 4 - 模块已经加载完成,准备执行 LOADED: 4, // 5 - 模块执行中 EXECUTING: 5, // 6 - 模块已执行完成 EXECUTED: 6 }
2,页面中主要部分是一个Module类
function Module(uri, deps) { this.uri = uri this.dependencies = deps || [] this.exports = null this.status = 0 // 依赖于本模块的模块 this._waitings = {} // 本模块未加载完的依赖数 this._remain = 0 }
3,大体结构如上,具体实现的话得先从seajs的入口seajs.use说起:
//可以看到,ids实际上是在preload的模块全部开始加载后才开始加载的 seajs.use = function(ids, callback) { Module.preload(function() { Module.use(ids, callback, data.cwd + "_use_" + cid()) }) return seajs }
seajs是个什么对象?
var seajs = global.seajs = { // The current version of Sea.js being used version: "@VERSION" }
global的传入是这样的:
(function(global, undefined) { // 在seajs多次加载时避免冲突 if (global.seajs) { return } })(this);
跟jquery很像,只是传入的是this,应该是为了兼容node等其他环境,在浏览器内,global就是window
4,module.preload
Module.preload = function(callback) { var preloadMods = data.preload var len = preloadMods.length if (len) { Module.use(preloadMods, function() { // 移除已经加载好的预加载模块 preloadMods.splice(0, len) // 允许预加载模块再次定义预加载模块(没用过……) Module.preload(callback) }, data.cwd + "_preload_" + cid()) } else { callback() } }
实际上它的过程是一直不断的读取需要预加载的内容,最后调用callback。
preload的内容,是从data.preload读取来的,在config.js文件内部:
data.preload = (function() { var plugins = [] // 这段代码获取url?后面的字段,并把"seajs-xxx"转换成"seajs-xxx=1" // 注意: 在uri或cookie内使用"seajs-xxx=1"标志来预加载"seajx-xxx"文件
var str = loc.search.replace(/(seajs-\w+)(&|$)/g, "$1=1$2") // 在字符串后面连接上cookie这一大段字符串 str += " " + doc.cookie // 将"seajs-xxx=1"的"seajs-xxx"存入plugins数组中,并返回
str.replace(/(seajs-\w+)=1/g, function(m, name) { plugins.push(name) }) return plugins })()
这段代码是preload的默认字段,另外里面有几个变量:
var data = seajs.data = {}
var doc = document var loc = location var cwd = dirname(loc.href) var scripts = doc.getElementsByTagName("script")
preload还有一部分是我们自己配置的,在config.js文件内,还有一段这样的代码:
seajs.config = function(configData) { //configData就是我们的所有配置,遍历之 for (var key in configData) { var curr = configData[key]//存储当前key的配置信息 var prev = data[key] // 如果之前有调用过seajs.config函数进行配置,那么把这些配置信息内的对象合并起来 if (prev && isObject(prev)) { for (var k in curr) { prev[k] = curr[k] } } else { //如果是数组的话,就concat起来 if (isArray(prev)) { curr = prev.concat(curr) } //确认base是个绝对路径 //如果base不是以"/"结尾,加上"/",addBase的作用是将相对路径转换成绝对路径 else if (key === "base") { (curr.slice(-1) === "/") || (curr += "/") curr = addBase(curr) } data[key] = curr } } //emit是触发事件的函数 emit("config", configData) return seajs }
至此,data这个对象是什么,config存哪了,就都清楚了,另外,其实在config.js内还有这样一个默认值:
//这段正则用来匹配path/seajs/xxx/xx/xx/...的path/部分然后存到data.base中 var BASE_RE = /^(.+?\/)(\?\?)?(seajs\/)+/ data.base = (loaderDir.match(BASE_RE) || ["", loaderDir])[1] data.dir = loaderDir data.cwd = cwd data.charset = "utf-8"
我们看到,module.preload调用了module.use,那module.use是个什么东西呢?
5,Module.use
Module.use = function (ids, callback, uri) { var mod = Module.get(uri, isArray(ids) ? ids : [ids]) //这个绑定在mod上的callback会在onload后触发,作用是获取各个依赖模块的输出,并且调用传入的callback,这个callback起真正作用的实际上是从seajs.use中传入的那个函数(如果有的话),因为Module.preload调用Module.use时传入的callback效果实际上在不断的调用自身,直到没有需要预加载的模块。实际效果是seajs.use(['a', 'b', 'c'], function(a, b, c){}) mod.callback = function() { 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) } delete mod.callback } mod.load() }
这里使用了一大堆未知方法,Module.get, mod.load, mod.exec, mod.resolve
6,Module.get
//如果存在则获取该Module对象,否则以模块url为id,存入cachedMods这个对象中,实际上所有新建的Module对象都存在这个对象中 Module.get = function(uri, deps) { return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps)) }
7,Module.prototype.resolve(mod.resolve)
//该方法获取所有依赖的完成id,然后返回,调用了Module.resolve方法 Module.prototype.resolve = function() { var mod = this var ids = mod.dependencies var uris = [] for (var i = 0, len = ids.length; i < len; i++) { uris[i] = Module.resolve(ids[i], mod.uri) } return uris }
可以看到,该方法依赖于Module.resolve方法
8,Module.resolve
Module.resolve = function(id, refUri) { // 为插件触发resolve事件比如text插件 var emitData = { id: id, refUri: refUri } emit("resolve", emitData) //id2Uri函数将id(比如相对路径)转换成完整的路径,作为存储id return emitData.uri || id2Uri(emitData.id, refUri) }
9,Module.prototype.load(mod.load)
这个方法是重头戏,而且有点长
Module.prototype.load = function() { var mod = this // 如果已经加载或正在被加载,不要重复加载 if (mod.status >= STATUS.LOADING) { return } mod.status = STATUS.LOADING // 为插件触发load事件,比如combo插件,获取依赖,存到uris变量中 var uris = mod.resolve() emit("load", uris) //获取依赖数 var len = mod._remain = uris.length var m // 依赖模块初始化,并给每个依赖的模块_waiting赋初值 for (var i = 0; i < len; i++) { m = Module.get(uris[i]) //没加载完的模块,添加_waiting if (m.status < STATUS.LOADED) { // 有可能重复依赖? m._waitings[mod.uri] = (m._waitings[mod.uri] || 0) + 1 } else { mod._remain-- } } //依赖加载完,则该模块加载完成,触发onload if (mod._remain === 0) { mod.onload() return } // 开始加载 var requestCache = {} for (i = 0; i < len; i++) { //事实上在resolve后在cachedMods对象中都存储了模块id m = cachedMods[uris[i]] //如果自身没加载,则加载该模块,调用了m.fetch,事实上fetch并没有真正执行内容获取,而仅仅是绑定发送请求的方法,requestCache的作用,是为下面的for循环做准备,具体看fetch。 if (m.status < STATUS.FETCHING) { m.fetch(requestCache) } else if (m.status === STATUS.SAVED) { m.load() } } // 这里才是真正执行该方法,这是为了解决 IE6-9的缓存bug , 具体请看github上的Issues#808 for (var requestUri in requestCache) { if (requestCache.hasOwnProperty(requestUri)) { requestCache[requestUri]() } } }
该方法内部调用了mod.fetch,mod.onload方法,除此之外,还有一个mod.exec方法在前面被Module.use依赖
10,Module.prototype.fetch(mod.fetch)
Module.prototype.fetch = function(requestCache) { var mod = this var uri = mod.uri mod.status = STATUS.FETCHING // 为combo等插件触发fetch事件 var emitData = { uri: uri } emit("fetch", emitData) var requestUri = emitData.requestUri || uri // 如果uri不符合规范或者已经加载好,直接加载依赖 if (!requestUri || fetchedList[requestUri]) { mod.load() return } //如果使用了combo插件,且在加载队列中有该页面的uri,只需要将该模块推入callback中,等待触发onload就是了 if (fetchingList[requestUri]) { callbackList[requestUri].push(mod) return } //否则,推入fetchingList,并初始化该uri的callbackList fetchingList[requestUri] = true callbackList[requestUri] = [mod] // 为text等插件触发request事件 emit("request", emitData = { uri: uri, requestUri: requestUri, onRequest: onRequest, charset: data.charset }) //如果还没有请求,且传入了requestCache对象,则将uri和发送函数对应推入该对象中,如果没有传入,直接启动发送 if (!emitData.requested) { requestCache ? requestCache[emitData.requestUri] = sendRequest : sendRequest() } //此处调用了request方法,作用是将uri作为一个link或者script标签 //的链接,onrequest函数是onload的回调函数,启动发送后将script标签删除 function sendRequest() { request(emitData.requestUri, emitData.onRequest, emitData.charset) } function onRequest() { //完成后将该模块在fetching列表中删除,推入fetched列表 delete fetchingList[requestUri] fetchedList[requestUri] = true // 此时模块运行,实际会有一个调用Module.define的过程,如果该模块是匿名模块,define会给anonymousMeta赋值 //如果是匿名模块,则调用Module.save if (anonymousMeta) { Module.save(uri, anonymousMeta) anonymousMeta = null } // 该requestUri的所有模块接收完成,调用其load方法 var m, mods = callbackList[requestUri] delete callbackList[requestUri] while ((m = mods.shift())) m.load() } }
该方法依赖于Module.save方法,实际上隐式依赖于Module.define方法
11,Module.define
Module.define = function (id, deps, factory) { var argsLen = arguments.length //这一长串ifelse逻辑是为了解析传入的参数 // define(factory) if (argsLen === 1) { factory = id id = undefined } else if (argsLen === 2) { factory = deps // define(deps, factory) if (isArray(id)) { deps = id id = undefined } // define(id, factory) else { deps = undefined } } // 如果没有申明依赖,并且传入了函数,则调用parseDependencies来解析该函数内容 if (!isArray(deps) && isFunction(factory)) { deps = parseDependencies(factory.toString()) } //此处尝试给模块命名id,如果没有成功,则生成匿名模块,等待在后期触发onrequest的时候在将其使用Module.save命名 var meta = { id: id, uri: Module.resolve(id), deps: deps, factory: factory } // 这个是尝试在ie6-9下面给uri复制,getCurrentScript为获取当前的script节点 if (!meta.uri && doc.attachEvent) { var script = getCurrentScript() if (script) { meta.uri = script.src } } // 为nocache插件触发define事件 emit("define", meta) //如果存在uri,则存储,否则,定义为匿名模块 meta.uri ? Module.save(meta.uri, meta) : anonymousMeta = meta }
这里,实际上如果在定义模块的时候没有使用数组存入依赖,很有可能会成为一个匿名模块,但是依赖它的模块会在require里面申明它的一个uri,并在load的时候调用Module.get使用该uri为该匿名模块创建了一个Module对象,所以在启动onrequest之前,若有匿名模块,则肯定是拥有该onrequest函数的模块,然后会在函数内部给该匿名模块命名。此处使用了Module.save。
12,Module.save
Module.save = function(uri, meta) { var mod = Module.get(uri) // 确认模块没被存储,如果是匿名模块,则meta.id是没有的 if (mod.status < STATUS.SAVED) { mod.id = meta.id || uri mod.dependencies = meta.deps || [] mod.factory = meta.factory mod.status = STATUS.SAVED } }
13,Module.prototype.onload(mod.onload)
//只有在所有依赖加载完后,才会调用onload Module.prototype.onload = function() { var mod = this mod.status = STATUS.LOADED //这不就是需要调用module.use内绑定的callback么 if (mod.callback) { mod.callback() } //检测依赖该模块的模块,_remain相应减少,如果为0,则调用onload var waitings = mod._waitings var uri, m for (uri in waitings) { if (waitings.hasOwnProperty(uri)) { m = cachedMods[uri] m._remain -= waitings[uri] if (m._remain === 0) { m.onload() } } } // 释放内存 delete mod._waitings delete mod._remain }
14,Module.prototype.exec(mod.exec)
这个放到最后,是因为一个模块在所有工作做完后,才会调用该方法
Module.prototype.exec = function () { var mod = this // 避免重复计算 if (mod.status >= STATUS.EXECUTING) { return mod.exports } mod.status = STATUS.EXECUTING // 这里创建了最常用的require函数 var uri = mod.uri //返回依赖模块的计算值exports function require(id) { return Module.get(require.resolve(id)).exec() } require.resolve = function(id) { return Module.resolve(id, uri) } //异步加载,触发callback require.async = function(ids, callback) { Module.use(ids, callback, uri + "_async_" + cid()) return require } var factory = mod.factory var exports = isFunction(factory) ? factory(require, mod.exports = {}, mod) : factory //有可能在factory是通过给exports复制来传递结果的 if (exports === undefined) { exports = mod.exports } // 出错,触发error事件(没有结果或者该模块是CSS文件) if (exports === null && !IS_CSS_RE.test(uri)) { emit("error", mod) } // 释放内存 delete mod.factory mod.exports = exports mod.status = STATUS.EXECUTED // 触发excec事件 emit("exec", mod) //最终计算出了该模块的返回值 return exports }