sea.js源码(Module.js核心代码)

源码版本: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
2
3
4
5
6
7
8
9
10
11
12
intro.js                // 源码顶部
outro.js // 源码底部
util-cs.js // (内部工具)获取当前加载中的script标签队形
util-deps.js // (内部工具)解析某一模块的依赖模块
util-events.js // (内部工具)sea.js的事件系统
util-lang.js // (内部工具)sea.js的类型判断
util-path.js // (内部工具)sea.js的路径解析
util-request.js // (内部工具)请求加载文件
standlone.js // (缩影代码)模块实现的简易版本
module.js // (核心代码)模块的实现
config.js // (核心代码)模块配置
```

每一个文件的代码都有它的作用,让我们继续看下去

module.js 代码解读

  • 以下内容第一遍可以快速浏览,然后在下面过程讲解中回头查阅相关变量属性的作用。

变量说明

1
2
3
4
5
6
var cachedMods = seajs.cache = {}
var anonymousMeta

var fetchingList = {}
var fetchedList = {}
var callbackList = {}

作用说明:

  • cachedMods :缓存队列,存储已经加载好的模块
  • anonymousMeta : 无factory的模块
  • fetchingList : 等待加载模块队列
  • fetchedlist : 加载中的模块队列
  • callbackList : 模块回调执行队列

模块的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var STATUS = Module.STATUS = {
// 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
}

分别为:

  1. FETCHING:开始从服务端加载模块
  2. SAVED:模块加载完成
  3. LOADING:加载依赖模块中
  4. LOADED:依赖模块加载完成
  5. EXECUTING:模块执行中
  6. EXECUTED:模块执行完成
  7. ERROR:模块加载出现错误

模块Module的构造函数

1
2
3
4
5
6
7
8
function Module(uri, deps) {
this.uri = uri
this.dependencies = deps || []
this.deps = {} // Ref the dependence modules
this.status = 0

this._entry = []
}

模块的构造函数:需要传入两个参数,模块的文件路径和依赖数组。

每个模块包含以下属性:

  • uri : 模块实例的标识,通常为一个模块的绝对路径
  • dependencies : 模块实例依赖的模块uri数组,里面放置的的是每个依赖模块的uri
  • dps : 模块实例依赖的模块对象数组,里面放置的是每个依赖模块对象Module实例。
  • status : 模块的状态,作为下一步如何处理模块的依据
  • _entry : 所依赖的模块加载完之后需要执行的 模块

模块Module上的函数方法

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
29
30
// Fetch a module
// 加载一个模块
Module.prototype.fetch = function(requestCache){
}

// Resolve id to uri
// 根据模块的 id 转化为 uri
Module.resolve = function(id, refUri) {
}

// Define a module
// 定义一个模块
Module.define = function (id, deps, factory) {
}

// Save meta data to cachedMods
// 将 Module 数据存储到 cachedMods 缓存中
Module.save = function(uri, meta) {
}

// Get an existed module or create a new one
// 获取一个已存在的模块 或 创建一个新的模块
Module.get = function(uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}

// Use function is equal to load a anonymous module
// 加载一个根节点模块
Module.use = function (ids, callback, uri) {
}

模块Module实例方法

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
29
// Resolve module.dependencies
// 解析模块的 dependencies 属性,解析转换为uri
Module.prototype.resolve = function() {
}

// Load module.dependencies and fire onload when all done
// 加载 模块依赖 ,并执行onload当所有的依赖模块加载完
Module.prototype.load = function() {
}

// Call this method when module is loaded
// 当模块加载好之后执行的方法
Module.prototype.onload = function() {
}

// Call this method when module is 404
// 当模块 404 的时候执行该方法
Module.prototype.error = function() {
}

// Execute a module
// 执行模块代码
Module.prototype.exec = function () {
}

// Fetch a module
// 加载一个模块资源
Module.prototype.fetch = function(requestCache) {
}

实例解析

让我们从一个实例开始看起:

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
29
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>sea.js解读测试</title>
</head>
<body>
</body>
<script src="../src/seajs-whole.js"></script>
<script>
console.log(seajs);
seajs.use(["./test"],function() {
console.log('root module');
});
</script>
</html>

<!--test.js-->
define(function(require, exports, module) {
var a1 = require('./a1');
console.log(a1.msg);
});

<!--a1.js-->
define(function(require, exports, module) {
var msg = "a1";
exports.msg = msg;
});

你可以先试着理解下面的流程,结合下面我们源码的详细分析理解。

image

module.js如何控制模块加载过程

模块的启动从 use 方法开始

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
29
30
31
32
33
// Get an existed module or create a new one
Module.get = function(uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}

// Use function is equal to load a anonymous module
Module.use = function (ids, callback, uri) {
var mod = Module.get(uri, isArray(ids) ? ids : [ids])

mod._entry.push(mod)
mod.history = {}
mod.remain = 1

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
delete mod.history
delete mod.remain
delete mod._entry
}

mod.load()
}
  • Module.get() 方法是获取一个存在的模块,如果该模块不存在,创建并返回一个新的模块。
    • 如果不记得 Module构造函数上属性和方法,返回上面查阅。
  • Module.use() 这里做了模块的初始化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    mod._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
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Load module.dependencies and fire onload when all done
// 加载 模块依赖 ,并执行onload当所有的依赖模块加载完
Module.prototype.load = function() {
var mod = this

// If the module is being loaded, just wait it onload call
if (mod.status >= STATUS.LOADING) {
return
}

mod.status = STATUS.LOADING

// Emit `load` event for plugins such as combo plugin
var uris = mod.resolve()
emit("load", uris)

for (var i = 0, len = uris.length; i < len; i++) {
mod.deps[mod.dependencies[i]] = Module.get(uris[i])
}

// Pass entry to it's dependencies
mod.pass()

// If module has entries not be passed, call onload
if (mod._entry.length) {
mod.onload()
return
}

// Begin parallel loading
var requestCache = {}
var m

for (i = 0; i < len; i++) {
m = cachedMods[uris[i]]

if (m.status < STATUS.FETCHING) {
m.fetch(requestCache)
}
else if (m.status === STATUS.SAVED) {
m.load()
}
}

// Send all requests at last to avoid cache bug in IE6-9. Issues#808
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
requestCache[requestUri]()
}
}
}

// 将最终需要执行的模块 一层一层 的传递下去,直至没有依赖
Module.prototype.pass = function() {
var mod = this

var len = mod.dependencies.length

for (var i = 0; i < mod._entry.length; i++) {
var entry = mod._entry[i]
var count = 0
for (var j = 0; j < len; j++) {
var m = mod.deps[mod.dependencies[j]]
// If the module is unload and unused in the entry, pass entry to it
if (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) {
entry.history[m.uri] = true
count++
m._entry.push(entry)
if(m.status === STATUS.LOADING) {
m.pass()
}
}
}
// If has passed the entry to it's dependencies, modify the entry's count and del it in the module
if (count > 0) {
entry.remain += count - 1
mod._entry.shift()
i--
}
}
}
  • Module.load() : 加载 模块依赖 ,并执行onload当所有的依赖模块加载完
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
Module.prototype.load = function() {
……
var uris = mod.resolve();
// 解析模块依赖的 uri 数组

mod.deps[mod.dependencies[i]] = Module.get(uris[i])
// 给模块的依赖Module对象数组 初始化,并用uri作为标志
……
pass();
/*
pass函数代码:
Module.prototype.pass = function() {
……
var len = mod.dependencies.length

for (var i = 0; i < mod._entry.length; i++) {
var entry = mod._entry[i];
……
for (var j = 0; j < len; j++) {
// 判断是否未加载,并且未传递过_entry
if (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) {
……
m._entry.push(entry)
//mod._entry中的模块会被传递给mod依赖的模块的_entry,
……
}
}
if(count>0){
entry.remain += count - 1
mod._entry.shift()
i--
//count 表示的就是还需要加载的模块
//将mod._entry中的每个模块的remain属性加上相应的依赖模块个数,然后将自己从_entry中移除。
// i-- ,使i减一,再次遍历,直至count <= 0
}
……
}
}
*/

// If module has entries not be passed, call onload
// 如果该模块已经没有依赖了
if (mod._entry.length) {
mod.onload()
return
}

// 并行加载文件
var requestCache = {}
// 缓存 需要加载的文件清单
for (i = 0; i < len; i++) {
m = cachedMods[uris[i]]

if (m.status < STATUS.FETCHING) {
// 如果模块还未从服务器加载过来,执行fetch().
// fetch() 将每个模块m从服务器获取对应的js加载到页面,并绑定到requestCache对象上
m.fetch(requestCache)
}
else if (m.status === STATUS.SAVED) {
// 如果模块已经加载成功,执行load()
m.load()
}
}
// Send all requests at last to avoid cache bug in IE6-9. Issues#808
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
requestCache[requestUri]()
// 遍历requestCache对象,并根据 URI 执行seajs.request
// seajs.request() 源码在util-request中,详细讲解在下方。
}
}

}

补充,pass()函数的目的就是 将_entry的模块传递给依赖的模块,通过依赖的模块再传递给依赖的模块所依赖的模块,一层层传递一下去,直到已经没有依赖的模块的那一层,这时候会调用mod.onload(),再根据remain判断是否所有的依赖已经加载完毕决定是否执行该特殊模块。

模块还未从服务加载: Module.fetch()

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Fetch a module
// 加载一个模块资源
Module.prototype.fetch = function(requestCache) {
var mod = this
var uri = mod.uri

mod.status = STATUS.FETCHING

// Emit `fetch` event for plugins such as combo plugin
var emitData = { uri: uri }
emit("fetch", emitData)
var requestUri = emitData.requestUri || uri

// Empty uri or a non-CMD module
if (!requestUri || fetchedList.hasOwnProperty(requestUri)) {
mod.load()
return
}

if (fetchingList.hasOwnProperty(requestUri)) {
callbackList[requestUri].push(mod)
return
}

fetchingList[requestUri] = true
callbackList[requestUri] = [mod]

// 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) : data.charset,
crossorigin: isFunction(data.crossorigin) ? data.crossorigin(requestUri) : data.crossorigin
})

if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}

function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset, emitData.crossorigin)
}

function onRequest(error) {
delete fetchingList[requestUri]
fetchedList[requestUri] = true

// Save meta data of anonymous module
if (anonymousMeta) {
Module.save(uri, anonymousMeta)
anonymousMeta = null
}

// Call callbacks
var m, mods = callbackList[requestUri]
delete callbackList[requestUri]
while ((m = mods.shift())) {
// When 404 occurs, the params error will be true
if(error === true) {
m.error()
}
else {
m.load()
}
}
}
}

在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
2
3
4
5
6
// If module has entries not be passed, call onload
// 如果该模块已经没有依赖了
if (mod._entry.length) {
mod.onload()
return
}
  • 上一节我们讲 pass() 将_entry的模块传递给依赖的模块,这样一层一层传递下去,当传到没有依赖的模块的时候,mod._entry.length 不为0假值。接下来中 mod.onload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Call this method when module is loaded
// 当模块加载好之后执行的方法
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED

// When sometimes cached in IE, exec will occur before onload, make sure len is an number
for (var i = 0, len = (mod._entry || []).length; i < len; i++) {
var entry = mod._entry[i]
if (--entry.remain === 0) {
entry.callback()
}
}

delete mod._entry
}

这段代码很好理解,将模块的状态转为 LOADED ,判断一下entry.remain是否为0,为0说明所依赖的所有模块已经加载完毕, 然后执行我们从最顶层的模块 一层一层传递过来的 _entry,即顶层特殊模块。

1
2
3
if (--entry.remain === 0) {
entry.callback()
}
  • 但是,事情还没完呢?让我们看看初始化模块是修改的回调函数是怎样的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
delete mod.history
delete mod.remain
delete mod._entry
}

我们可以看到

1
2
3
4
5
6
7
8
for (var i = 0, len = uris.length; i < len; i++) {
exports[i] = cachedMods[uris[i]].exec()
// 需要执行前面我们所缓存加载过来的模块,赋值给一个数组以存储各模板接口。
}
if (callback) {
callback.apply(global, exports)
// 将接口数组作为参数传递,并将我们定义的函数放到全局作用域执行
}

理所应该,我们接下来就应该解读 exec() 这个执行模块的方法。但是我们必须了解 Module.define(ids,deps,factory(require,exports,module)) 这个在js文件中定义模块的方法。

这里Module.define(),Module.exec()我独立开来,有兴趣的小伙伴请移步seajs源码(define,exec定义和执行模块代码)

总结

模块的加载过程:
image