由上可知,a模块依赖b,b依赖c.
说明: globalModule 为seajs初始化时(引入sea.js时),Module的实例 var globalModule = new Module(util.pageUri,STATUS.COMPILED)
此时 ids -> ['./a','jquery'], callback -> function(a,$){var num = a.a;$('#J_A').text(num);}
接下来将调用 globalModule._use(ids,callback)
调用_load
方法。
//util.map : 让数据成员全部执行一次一个指定的
函数,并返回一个新的数组,该数组为原数组成员执行回调后的结果
var args = util.map(uris,function(uri) {
return uri ? cachedModules[uri]._compile() : null;//如果存在url,就调用_compile方法。
})
if (callback) { callback.apply(null,args) }
})
}
因为调用_load方法后,会出现两个回调函数,因此我们将function(a,$){var num = a.a;$('#J_A').text(num);}标志为callback1,
把this._load(uris,function() { })回调方法标志为callback2.
resolve方法就是解析模块地址的,这里我就不细讲了。
最终var uris = resolve(ids,this.uri)中 的uris被解析成了['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js'],模块路径解析已经完毕。
而接下来将执行this._load
方法主要会先判断哪些资源
文件还没有ready,如果全部资源
文件都处于ready状态就执行callback2
// 在这其中还会做循环依赖的判断,以及对没有加载的js执行加载
Module.prototype._load = function(uris,callback2) {
//util.filter : 让数据成员全部执行一次一个指定的
函数,并返回一个新的数组,该数组为原数组成员执行回调后返回为true的成员
//unLoadedUris是那些没有被编译的模块uri数组
var unLoadedUris = util.filter(uris,function(uri) {
//返回执行
函数布尔值为true的成员,在uri存在并且在内部变量cacheModules中不存在或者它在存储信息中status的值小于STATUS.READY时返回true
// STATUS.READY值为4,小于四则可能的情况是
获取中,下载中。
return uri && (!cachedModules[uri] ||
cachedModules[uri].status < STATUS.READY)
});
//如果uris中的模块全部都ready好了,执行回调并
退出函数体(这时就会
调用模块的_compile
方法了)。
var length = unLoadedUris.length
if (length === 0) { callback2() return }
//还未加载的模块个数
var remain = length
//创建闭包,尝试去加载那些没有加载的模块
for (var i = 0; i < length; i++) {
(function(uri) {
//判断如果在内部变量cachedModules里面并不存在该uri的存储信息则实例化一个Module对象
var module = cachedModules[uri] ||
(cachedModules[uri] = new Module(uri,STATUS.FETCHING))
//如果模块的状态值大于等于2,也就意味着模块已经被下载好并已经存在于本地了,这个时候执行onFetched()
//否则则调用fetch(uri,onFetched) ,尝试下载资源文件,资源文件下载后会触发onload,onload中会执行回调onFetched的方法。
module.status >= STATUS.FETCHED ? onFetched() : fetch(uri,onFetched)
function onFetched() {
module = cachedModules[uri]
//当模块的状态值为大于等于STATUS.SAVED的时候,也就意味着该模块所有的依赖信息已经被拿到
if (module.status >= STATUS.SAVED) {
//getPureDependencies:得到不存在循环依赖的依赖数组
var deps = getPureDependencies(module)
//如果依赖数组不为空
if (deps.length) {
//再次执行_load()
方法,直到全部依赖加载完成
后执行回调
Module.prototype._load(deps,function() {
cb(module)
})
}
//如果依赖数组为空的情况下,直接执行cb(module)
else {
cb(module)
}
}
// 如果
获取失败后,比如
404或者不符合模块化规范
//在这种情形下,module.status会维持在 FETCHING 或者 FETCHED
else {
cb()
}
}
})(unLoadedUris[i])
}
// cb
方法 - 加载完所有模块执行回调
function cb(module) {
// 如果module的存储信息存在,那么
修改它的module存储信息中的status的值,
修改为 STATUS.READY
module && (module.status = STATUS.READY)
// 只有当所有模块加载完毕
后执行回调。
--remain === 0 && callback2()
}
}
}
这里unLoadedUris的数组长度为2 ,['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js'],所以 接下来会产生两个以 js路径为名称的闭包。
以为例
接下来 : 首先会创建一个Module:
= STATUS.FETCHED ? onFetched() : fetch(uri,onFetched)
因为此时a模块并没有加载 所以接下来将会执行 fetch(uri,onFetched) 即fetch('http://localhost/test/SEAJS/a.js',onFetched)。
获取列表中查找是否含有requestUri记录
if (fetchedList[requestUri]) {
// 这个时候将原始uri的module存储信息刷新到通过map重定义的requestUri上
cachedModules[uri] = cachedModules[requestUri]
// 执行onFetched 并返回,意味着模块已经
获取成功了
onFetched()
return
}
//在
获取列表中
查询 requestUri 的存储信息
if (fetchingList[requestUri]) {
//在callbacklist中加入该uri对应下的callback,并返回
callbackList[requestUri].push(onFetched) //如果正在
获取中,就把此模块的onFetched回调
方法push进数组中,并返回。
return
}
// 如果尝试
获取的模块都未出现在fetchedList和fetchingList中,则分别在请求列表和回调列表中
添加其信息
fetchingList[requestUri] = true
callbackList[requestUri] = [onFetched]
// Fetches it
Module._fetch(
requestUri,
function() {
fetchedList[requestUri] = true
// Updates module status
// 如果 module.status 等于 STATUS.FECTCHING,则
修改module状态为FETCHED
var module = cachedModules[uri]
if (module.status === STATUS.FETCHING) {
module.status = STATUS.FETCHED
}
if (fetchingList[requestUri]) {
delete fetchingList[requestUri]
}
// Calls callbackList 统一执行回调
if (callbackList[requestUri]) {
util.forEach(callbackList[requestUri],function(fn) {
fn() //fn就是模块a对应的onFeched
方法。
})
delete callbackList[requestUri]
}
},
config.charset
)
}
接下来 将会执行 Module._fetch(),这里的回调函数我们称作为callback3.
此方法就是调用loadJs方法动态下载a.js文件。(因为有a和jquery,所以会新建两个script),这里有一个疑问,新建a的script,并添加到head中,就会下载js文件,但是在seajs中,并没有下载,而是等jquery的script建立好,并添加到head中,才会下载(谷歌调试器设断点,一直显示pending等待中)。这是为毛?
(推荐看这里:,这里我说下额外的问题,大家可能知道为什么我们要少用table来布局,因为table在呈现树布局的时候,需要多次计算,而div只需要一次。同时,美的电商面试官告诉我:table需要全部解析完才会显示出来,而div解析多少就显示多少。经查证table中如果有tbody标签,就会按照tbody来分段显示。因此在IE6,7,8中,如果你用innerHTML来创建一个"
。)。
下载成功后,就会解析执行,执行的是define方法。这里会先执行a模块的代码。
define(id,deps,function(){})方法解析
函数
if (!util.isArray(deps) && util.isFunction(factory)) { //
函数体内正则匹配require字符串,并形成数组返回赋值给deps
deps = util.parseDependencies(factory.toString())
}
//设置元信息
var
Meta = { id: id,dependencies: deps,factory: factory }
if (document.attachEvent) {
// 得到当前script的节点
var script = util.getCurrentScript()
// 如果script节点存在
if (script) {
// 得到原始uri地址
derivedUri = util.unParseMap(util.getScriptAbsoluteSrc(script)) }
if (!derivedUri) {
util.log('
Failed to derive URI from interactive script for:',factory.toString(),'warn')
}
}
.........
}
define首先会对factory执行一个判断 ,判断它是否为一个函数(原因是因为define内也可以包括文件,对象)
如果是函数 , 那么 就会通过factory.toString(),得到函数,并通过正则匹配得 a.js的依赖,并把依赖保存在 deps 中
对于 a.js 而言, 它的依赖 是 b.js 所以 deps为 ['./b']
并对 a.js 的信息进行保存 var Meta = { id: id,factory: factory }
针对a.js Meta = { id : undefined,dependencies : ['./b'],factory : function(xxx){xxx}}
在 ie 6-9 浏览器中可以拿到当前运行js的路径 但是在标准浏览器中 ,这不可行 ,所以暂时先把元信息赋值给anonymousModuleMeta = Meta。
然后触发onload,这时就会调用回调方法callback3,此回调方法就会修改当前回调模块(a.js)的状态值,将其设置为 module.status = STATUS.FETCHED。
再接下来,将统一 执行回调队列 callbackList 中的 a.js所对应的回调,也就是onFetched。
onFetched方法会检查a模块是否有依赖模块,因为a依赖于b,所以对模块a所依赖的b.js 执行_load()。
会去下载b模块,这时会先执行jquery的define方法。因为jquery没依赖模块,所以onload回调后。onFetched调用cb方法。
当b按照a一样的过程实现后,就会下载c模块。最终c,b,a模块都下载执行define,并onload结束后,也会调用cb方法,(先c,再b,后c)
所有模块都为ready之后,就会调用callback2方法。
最终回调到callback2,执行a和jquery模块的_compile方法:
首先编译a.js模块,模块a的function执行,因为a里面有require(b.js),因此会去执行b模块的function.
模块 a 的function开始执行
模块 b 的function开始执行
模块 c 的function开始执行
模块 c 的function执行完毕
模块 b 的function执行完毕
模块 a 的function执行完毕
最后执行jquery的function。
编译结束后,就执行callback1,就可以使用a和jquery对象了。
PS:seajs版本已经更新,现在没有_compile方法了。(大家自行去看,我也要去看下)
接着讲下seajs的模块编译_compile过程。
首先是a.js的编译
404.
134 // 2. the module file is not written with valid module format.
135 // 3. other error cases.
136 // 这里是处理一些异常情况,此时直接返回null
137 if (module.status < STATUS.SAVED && !hasModifiers(module)) {
138 return null
139 }
140 // 更改模块状态为COMPILING,表示模块正在编译
141 module.status = STATUS.COMPILING
142
143 // 模块内部使用,是一个
方法,用来
获取其他模块提供(称之为子模块)的接口,同步操作
144 function require(id) {
145 // 根据id解析模块的路径
146 var uri = resolve(id,module.uri)
147 // 从模块缓存中
获取模块(注意,其实这里子模块作为主模块的依赖项是已经被下载下来的)
148 var child = cachedModules[uri]
149
150 // Just return null when uri is invalid.
151 // 如果child为空,只能表示参数填写出错导致uri不正确,那么直接返回null
152 if (!child) {
153 return null
154 }
155
156 // Avoids circular calls.
157 // 如果子模块的状态为STATUS.COMPILING,直接返回child.exports,避免因为循环依赖反复编译模块
158 if (child.status === STATUS.COMPILING) {
159 return child.exports
160 }
161 // 指向初始化时
调用当前模块的模块。根据该
属性,可以得到模块初始化时的Call Stack.
162 child.parent = module
163 // 返回编译过的child的module.exports
164 return child._compile()
165 }
166 // 模块内部使用,用来异步加载模块,并在加载完成
后执行指定回调。
167 require.async = function(ids,callback) {
168 module._use(ids,callback)
169 }
170 // 使用模块系统内部的路径解析机制来解析并返回模块路径。该
函数不会加载模块,只返回解析后的
绝对路径。
171 require.resolve = function(id) {
172 return resolve(id,module.uri)
173 }
174 // 通过该
属性,可以查看到模块系统加载过的所有模块。
175 // 在某些情况下,如果需要重新加载某个模块,可以得到该模块的 uri,然后通过 delete require.cache[uri] 来将其信息
删除掉。这样下次 使用时,就会重新
获取。
176 require.cache = cachedModules
177
178 // require是一个
方法,用来
获取其他模块提供的接口。
179 module.require = require
180 // exports是一个对象,用来向外提供模块接口。
181 module.exports = {}
182 var factory = module.factory
183
184 // factory 为
函数时,表示模块的构造
方法。执行该
方法,可以得到模块向外提供的接口。
185 if (util.isFunction(factory)) {
186 compileStack.push(module)
187 runInModuleContext(factory,module)
188 compileStack.pop()
189 }
190 // factory 为对象、字符串等非
函数类型时,表示模块的接口就是该对象、字符串等值。
191 // 如:define({ "foo": "bar" });
192 // 如:define('I am a template. My name is {{name}}.');
193 else if (factory !== undefined) {
194 module.exports = factory
195 }
196
197 // 更改模块状态为COMPILED,表示模块已编译
198 module.status = STATUS.COMPILED
199 // 执行模块接口
修改,通过seajs.modify()
200 execModifiers(module)
201 return module.exports
202 }
这里就是把module.export进行初始化。runInModuleContext方法:
代码
489 function runInModuleContext(fn,module) {
490 // 传入与模块相关的两个参数以及模块自身
491 // exports用来暴露接口
492 // require用来
获取依赖模块(同步)(编译)
493 var ret = fn(module.require,module.exports,module)
494 //
支持返回值暴露接口形式,如:
495 // return {
496 // fn1 : xx
497 //,fn2 : xx
498 // ...
499 // }
500 if (ret !== undefined) {
501 module.exports = ret
502 }
503 }
执行a.js中的function方法,这时会调用var b = require("b.js"),
require方法会返回b的compile方法的返回值,b模块中又有var c = require('c.js')。
这时会调用c的compile方法,然后调用c的function,c中,如果要暴露对象,或者是return 对象c,则模块c的exports = c。或者直接是module.export = c;总之最后会返回module c.export = c;所以var c = module c.export = c,模块b中,就可以使用变量c调用模块c中的c对象的方法和属性。
以此类推,最终a模块也能调用b模块中b对象的属性和方法。
不管什么模块,只要使用了module.export = xx模块,其他模块就可以使用require("xx模块"),调用xx模块中的各种方法了。
最终模块的状态会变成module.status = STATUS.COMPILED。
这时args = [module a.export,module jquery.export];
这时function中的a和$就是module a.export和module jquery.export。
因为本人现在在研究jquery源码和jquery框架设计,因此共享一些经验:
jquery源码,我在网上看了很多解析,看着看着就看不下去了。意义不大,推荐妙味课堂的jquery源码解析。
司徒正美的javascript框架设计,个人觉得难度大,但是精读后,你就是高级前端工程师了。
玉伯的sea.js,我建议去学习,去用,毕竟是中国人自己做的。我们公司新的项目或者重构,都会使用seajs来做。
接下来就是模块化handbars以及mvc的backbone或者mvvm的angular的源码精读。这里我希望有人给我提建议,看什么书,看什么网站,看什么视频能够快速的学习。