源码版本:3.0.0 sea.js
声明:水平有限,读者请客观阅读本博文。如有对源码解读错误的地方,请不吝指出。
讲解的风格是先列源码,然后把源码的执行过程表示出来,细节实现请读者阅读讲解上方的源码。
建议读者先行阅读一遍源码再理解这篇博文,至少做到理解seajs框架大体的函数工具是做什么。
前言
在阅读sea.js源码前,我们先来简单看看什么是 JavaScript模块化。
现在的前端开发, 愈来愈趋向于桌面应用,需要团队合作,管理。开发一个新的页面,我们可能需要加载其他别人写好的模块,这个时候,我们就需要javascript模块化。
模块:简单来说就是实现特定功能的一组方法。
说到当前对于javascript模块化实现比较完善,理所当然我们会想到,common.js,AMD,CMD,ES6的Module。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。对于依赖的模块,CMD是延迟执行,并推崇依赖就近。
sea.js整体架构
sea.js3.0.0版本,源码整体代码(算上注释,回车符)有1000来行。从github上clone下来,我们可以发现src下面有这么几个文件。
1 | intro.js // 源码顶部 |
每一个文件的代码都有它的作用,让我们继续看下去
module.js 代码解读
- 以下内容第一遍可以快速浏览,然后在下面过程讲解中回头查阅相关变量属性的作用。
变量说明
1 | var cachedMods = seajs.cache = {} |
作用说明:
- cachedMods :缓存队列,存储已经加载好的模块
- anonymousMeta : 无factory的模块
- fetchingList : 等待加载模块队列
- fetchedlist : 加载中的模块队列
- callbackList : 模块回调执行队列
模块的状态
1 | var STATUS = Module.STATUS = { |
分别为:
- FETCHING:开始从服务端加载模块
- SAVED:模块加载完成
- LOADING:加载依赖模块中
- LOADED:依赖模块加载完成
- EXECUTING:模块执行中
- EXECUTED:模块执行完成
- ERROR:模块加载出现错误
模块Module的构造函数
1 | function Module(uri, deps) { |
模块的构造函数:需要传入两个参数,模块的文件路径和依赖数组。
每个模块包含以下属性:
- uri : 模块实例的标识,通常为一个模块的绝对路径
- dependencies : 模块实例依赖的模块uri数组,里面放置的的是每个依赖模块的uri
- dps : 模块实例依赖的模块对象数组,里面放置的是每个依赖模块对象Module实例。
- status : 模块的状态,作为下一步如何处理模块的依据
- _entry : 所依赖的模块加载完之后需要执行的 模块
模块Module上的函数方法
1 | // Fetch a module |
模块Module实例方法
1 | // Resolve module.dependencies |
实例解析
让我们从一个实例开始看起:
1 | <!--index.html--> |
你可以先试着理解下面的流程,结合下面我们源码的详细分析理解。
module.js如何控制模块加载过程
模块的启动从 use 方法开始
1 | // Get an existed module or create a new one |
- Module.get() 方法是获取一个存在的模块,如果该模块不存在,创建并返回一个新的模块。
- 如果不记得 Module构造函数上属性和方法,返回上面查阅。
- Module.use() 这里做了模块的初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19mod._entry.push(mod)
// 先是将自身模块放置到_entry,为了传递给后面依赖模块,后面会讲。
mod.remian = 1
// 表示还有多个依赖没有加载,这里表示的启动文件。use引入的文件。
mod.callback = function() {
……
var uris = mod.resolve()
// 解析模块依赖的 uri 数组
……
exports[i] = cachedMods[uris[i]].exec()
// 执行依赖接口并将接口存储在一个数组中
//
……
// 将依赖模块暴露的接口传递过来。
}
// 这里面是回调进行处理
然后执行 mod.load()
初始完Module ,执行Module.load()
1 | // Load module.dependencies and fire onload when all done |
- Module.load() : 加载 模块依赖 ,并执行onload当所有的依赖模块加载完
1 | Module.prototype.load = function() { |
补充,pass()函数的目的就是 将_entry的模块传递给依赖的模块,通过依赖的模块再传递给依赖的模块所依赖的模块,一层层传递一下去,直到已经没有依赖的模块的那一层,这时候会调用mod.onload(),再根据remain判断是否所有的依赖已经加载完毕决定是否执行该特殊模块。
模块还未从服务加载: Module.fetch()
1 | // Fetch a module |
在fetch(),
(一)为 当前模块在 fetchingList中设置标识,存储到 callbackList 数组中。
(二)将sendRequest 函数添加到requestCache对象,用于调用加载模块。
【sendRequest() 方法是从服务器请求资源到页面中,调用seajs.request()。那么这个seajs.request如何将模块文件请求到页面呢?具体详情请查看:sea.js源码(seajs.request()请求资源)】
- 过程重点介绍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28……
fetchingList[requestUri] = true
callbackList[requestUri] = [mod]
……
emit("request", emitData = {
uri: uri,
requestUri: requestUri,
onRequest: onRequest,
charset: isFunction(data.charset) ? data.charset(requestUri) : data.charset,
crossorigin: isFunction(data.crossorigin) ? data.crossorigin(requestUri) : data.crossorigin
})
/*
* uri : 资源uri,一般为相对路径
* requestUri : 模块资源路径
* onRequest : 请求到资源的时候的回调
* charset : 文件编码
* crossorigin : 设置跨域属性,配置元素获取数据的CORS请求
*/
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
// 给 requestCache 对象添加属性sendRequest函数 ,以requestUri为标识,
}
function sendRequest() {}
function onRequest(error) {}
当模块已经没有依赖了, 执行onload()
1 | // If module has entries not be passed, call onload |
- 上一节我们讲 pass() 将_entry的模块传递给依赖的模块,这样一层一层传递下去,当传到没有依赖的模块的时候,
mod._entry.length
不为0假值。接下来中mod.onload
1 | // Call this method when module is loaded |
这段代码很好理解,将模块的状态转为 LOADED ,判断一下entry.remain是否为0,为0说明所依赖的所有模块已经加载完毕, 然后执行我们从最顶层的模块 一层一层传递过来的 _entry,即顶层特殊模块。
1 | if (--entry.remain === 0) { |
- 但是,事情还没完呢?让我们看看初始化模块是修改的回调函数是怎样的?
1 | mod.callback = function() { |
我们可以看到
1 | for (var i = 0, len = uris.length; i < len; i++) { |
理所应该,我们接下来就应该解读 exec() 这个执行模块的方法。但是我们必须了解 Module.define(ids,deps,factory(require,exports,module))
这个在js文件中定义模块的方法。
这里Module.define(),Module.exec()
我独立开来,有兴趣的小伙伴请移步seajs源码(define,exec定义和执行模块代码)
总结
模块的加载过程: