Jealh's Blog

𝓣𝓱𝓲𝓼 𝓲𝓼 𝓪 𝓫𝓮𝓪𝓾𝓽𝓲𝓯𝓾𝓵 𝓼𝓾𝓫𝓽𝓲𝓽𝓵𝓮

0%

seajs源码阅读

介绍:js 模块加载器。这个开源项目应该是多年前的了,最新版本也是在 2014-03-06 的 2.2.0 版本了。所以,对于现在的浏览器,使用 script 标签的 type=”module” es6-in-depth-modules,便可以使用模块化了。

这篇起因是刷到了玉伯大佬离职的消息,了解到 seajs 这个模块加载器是他写的,所以就阅读了一手,如有冒犯,恳请原谅。

依旧延续上次的方法,我们先从使用方式开始。

1
2
3
4
5
6
7
// 定义模块
define(function (require, exports, module) {
var $ = require("jquery");
var Spinning = require("./spinning");

exports.moduleParam = "param";
});

上面就是 Sea.js 推荐的 CMD 模块书写格式。如果你有使用过 Node.js,一切都很自然。

文件目录结构

内部使用了封装的 seatools npm 包,使用的是 Grunt 打包,使用了其中的 contact file 功能。其会进行文件合并操作,所以会见到源码的 intro.js 与 outro.js 那种不完整的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
concat: {
dist: {
src: [
'src/intro.js',
'src/sea.js',
'src/util-lang.js',
'src/util-events.js',
'src/util-path.js',
'src/util-request.js',
'src/util-cs.js',
'src/util-deps.js',
'src/module.js',
'src/config.js',
'src/outro.js'
],
dest: 'dist/sea-debug.js'
},
...
}

发布订阅

seajs 使用了发布订阅模式,内置了如下事件:

  • config
  • load
  • exec
  • fetch
  • request
  • resolve
  • define
  • save
  • error

如需要处理这些事件,可以使用 seajs.on 添加对应事件回调。这些事件会在特定的时机触发,如 config,会在 seajs.config 函数里触发。这种非常适合插件系统

seajs 全局对象

img

seajs.config

进行配置,配置的属性会保留在 seajs.data 上面

1
2
3
4
5
6
7
8
9
10
11
12
13
seajs.config({
// 设置路径,方便跨目录调用
paths: {
arale: "https://a.alipayobjects.com/arale",
jquery: "https://a.alipayobjects.com/jquery",
},

// 设置别名,方便调用
alias: {
class: "arale/class/1.0.0/class",
jquery: "jquery/jquery/1.10.1/jquery",
},
});

seajs.request

分为 webWorker 与 非 webWorker 环境,通过importScripts判断是否是 webWorker 环境

  • 非 webWorker

非 worker 环境使用 script 标签导入。 并使用 async 属性并行请求脚本。同时添加 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
var doc = document;
var head =
doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement;
var baseElement = head.getElementsByTagName("base")[0];

function request(url, callback, charset, crossorigin) {
var node = doc.createElement("script");

if (charset) {
node.charset = charset;
}

if (!isUndefined(crossorigin)) {
node.setAttribute("crossorigin", crossorigin);
}

addOnload(node, callback, url);

node.async = true;
node.src = url;

// For some cache cases in IE 6-8, the script executes IMMEDIATELY after
// the end of the insert execution, so use `currentlyAddingScript` to
// hold current node, for deriving url in `define` call
currentlyAddingScript = node;

// ref: #185 & http://dev.jquery.com/ticket/2709
baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node);

currentlyAddingScript = null;
}

function addOnload(node, callback, url) {
var supportOnload = "onload" in node;

if (supportOnload) {
node.onload = onload;
node.onerror = function () {
emit("error", { uri: url, node: node });
onload(true);
};
} else {
node.onreadystatechange = function () {
if (/loaded|complete/.test(node.readyState)) {
onload();
}
};
}

function onload(error) {
// Ensure only run once and handle memory leak in IE
node.onload = node.onerror = node.onreadystatechange = null;

// Remove the script to reduce memory leak
if (!data.debug) {
head.removeChild(node);
}

// Dereference the node
node = null;

callback(error);
}
}
  • webWorker

webWorker 环境下,使用 importScripts 导入 js 脚本

1
2
3
4
5
6
7
8
9
10
function requestFromWebWorker(url, callback, charset, crossorigin) {
// Load with importScripts
var error;
try {
importScripts(url);
} catch (e) {
error = e;
}
callback(error);
}

seajs.data

一个对象,存放一些路径、charset 等信息。

  • base: root path for id2url
  • dir: loader’s full path
  • cwd: current working directory
  • charset: charset for requesting files
  • crossorigin: The CORS options, Don’t set CORS on default.
  • alias: An object containing shorthands of module id
  • paths: An object containing path shorthands in module id
  • vars: The {xxx} variables in module id
  • map: An array containing rules to map module uri
  • debug: Debug mode. The default value is false

seajs.resolve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function id2Uri(id, refUri) {
if (!id) return "";

id = parseAlias(id); // 如果有 Alias, 就使用 alias 中的路径
id = parsePaths(id); // 如果 data.paths 中有,则使用 paths 中的路径
id = parseAlias(id);
id = parseVars(id);
id = parseAlias(id);
id = normalize(id);
id = parseAlias(id);

var uri = addBase(id, refUri);
uri = parseAlias(uri);
uri = parseMap(uri);

return uri;
}

define

由开始的例子我们可以看到,全局定义了 define 方法,其作用是用来定义模块。那我们就从 define 开始。在谈 define 前,我们要先看看 Module 对象,因为 define 取的是 Module 的 define 方法 😂。

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
// 赋值到window
global.define = Module.define;

// define 模块
Module.define = function (id, deps, factory) {
var argsLen = arguments.length;

// 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;
}
}

// Parse dependencies according to the module factory code
if (!isArray(deps) && isFunction(factory)) {
deps =
typeof parseDependencies === "undefined"
? []
: parseDependencies(factory.toString());
}

var meta = {
id: id,
uri: Module.resolve(id),
deps: deps,
factory: factory,
};

// Try to derive uri in IE6-9 for anonymous modules
if (
!isWebWorker &&
!meta.uri &&
doc.attachEvent &&
typeof getCurrentScript !== "undefined"
) {
var script = getCurrentScript();

if (script) {
meta.uri = script.src;
}

// NOTE: If the id-deriving methods above is failed, then falls back
// to use onload event to get the uri
}

// Emit `define` event, used in nocache plugin, seajs node version etc
emit("define", meta);

meta.uri
? Module.save(meta.uri, meta)
: // Save information for "saving" work in the script onload event
(anonymousMeta = meta);
};

define 支持多种参数,其判断依据是根据参数长度

  • define(factory)
  • define(deps, factory)
  • define(id, factory)

在 deps 不为数组时,会使用 parseDependencies 方法将 factory 里的 require 依赖的提取出来。

在 Module.resolve 中,使用 seajs.resolve 转换路径。同时创建 meta 对象,保存模块的 id, uri, deps, factory。然后执行 Module.save 方法,将 mod 保存在 cachedMods 对象里边。 那 factory 会在什么时候执行呢,继续看。

当我们定义好模块后,那接下来我们就应该去使用模块了。

seajs.use

1
2
3
4
seajs.use = function (ids, callback) {
Module.use(ids, callback, data.cwd + "_use_" + cid());
return seajs;
};

内部调用 Module.use 静态方法, 传入 ids, callback, uri。

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 = [];
}

其提供了如下静态方法:

  • resolve
  • define
  • save
  • get
  • use

那什么时候去创建 Module 实例呢,是在 seajs.use 时。

Module.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
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 从 cachedMods 获取 module。将自身添加到 _entry 里,最后执行 mod 的 load 方法, 也就是 Module.prototype.load

Module.save

将 meta 对象数据保存在 cachedMods 里。

1
2
3
4
5
6
7
8
9
10
11
12
13
Module.save = function (uri, meta) {
var mod = Module.get(uri);

// Do NOT override already saved modules
if (mod.status < STATUS.SAVED) {
mod.id = meta.id || uri;
mod.dependencies = meta.deps || [];
mod.factory = meta.factory;
mod.status = STATUS.SAVED;

emit("save", mod);
}
};

Module.get

1
2
3
Module.get = function (uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps));
};

Module.resolve

1
2
3
4
5
6
7
8
// Resolve id to uri
Module.resolve = function (id, refUri) {
// Emit `resolve` event for plugins such as text plugin
var emitData = { id: id, refUri: refUri };
emit("resolve", emitData);

return emitData.uri || seajs.resolve(emitData.id, refUri);
};

Module.prototype.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
// Load module.dependencies and fire onload when all done
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]();
}
}
};

先判断 mod 的 status 是否是加载中,如果是,就由 emit 事件处理。然后通过 Module.prototype.resolve 获取依赖模块的 uri 列表。赋值给 mod.deps。mod.dependencies 在 factory 函数就会通过正则获取依赖。mod.deps 就形如 {"jquery": Module }。接下来就调用 Module.prototype.pass 进行 entry 到 dependence 的转换。如果 dependence 里面都 loaded 了,就触发 Module.prototype.onload。最后判断 cachedMods 的状态,如果 小于 FETCHING 就开始并行加载,调用 Module.prototype.fetch,如果状态等于 SAVED 就调用 Module.prototype.load

Module.prototype.resolve

1
2
3
4
5
6
7
8
9
10
11
// Resolve module.dependencies
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;
};

获取 dependencies 的 uri

Module.prototype.pass

将当前 mod 的 _entry 添加到 dependcies 的_entry 里边。

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
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--;
}
}
};

这段代码先获取mod.dependencies.length。然后遍历 mod._entry, 二层循环遍历 mod.dependencies,如果 dependencies 里有未加载的且 entry.history 没有保留,那么就将 entry push 到当前 dependence 的 _entry 里,如果当前 dependence 的状态是 loading, 就 递归 pass。

Module.prototype.onload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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;
};

将 mod 的 status 变成 STATUS.LOADED。在 mod.remain 为 1 时,也就是只有 mod 自己时,执行 mod.callback

Module.prototype.exec

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
Module.prototype.exec = function () {
var mod = this;

// When module is executed, DO NOT execute it again. When module
// is being executed, just return `module.exports` too, for avoiding
// circularly calling
if (mod.status >= STATUS.EXECUTING) {
return mod.exports;
}

mod.status = STATUS.EXECUTING;

if (mod._entry && !mod._entry.length) {
delete mod._entry;
}

//non-cmd module has no property factory and exports
if (!mod.hasOwnProperty("factory")) {
mod.non = true;
return;
}

// Create require
var uri = mod.uri;

function require(id) {
var m = mod.deps[id] || Module.get(require.resolve(id));
if (m.status == STATUS.ERROR) {
throw new Error("module was broken: " + m.uri);
}
return m.exec();
}

require.resolve = function (id) {
return Module.resolve(id, uri);
};

require.async = function (ids, callback) {
Module.use(ids, callback, uri + "_async_" + cid());
return require;
};

// Exec factory
var factory = mod.factory;

var exports = isFunction(factory)
? factory.call((mod.exports = {}), require, mod.exports, mod)
: factory;

if (exports === undefined) {
exports = mod.exports;
}

// Reduce memory leak
delete mod.factory;

mod.exports = exports;
mod.status = STATUS.EXECUTED;

// Emit `exec` event
emit("exec", mod);

return mod.exports;
};

执行 factory 函数,返回一个对象,包含 factory 中 exports 的变量。
在 exec 中,会判断 mod 的状态如果是 EXECUTING 或者 EXECUTED,就直接返回 mod.exports, 避免了重复执行。再次判断 mod._entry, 如果为空就 delete 掉。这里会将 mod.exports 与 factory 中改变的 exports 进行一个同步。

Module.prototype.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
71
72
73
74
75
76
77
78
79
80
81
// 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();
}
}
}
};

根据 mod.uri 进行 fetch 操作,如果 uri 为空或者, fetchedList 已经 fetch 了,就执行 mod.load。这里的 request 就直接执行 seajs.request。进行脚本请求。最后执行 Module.prototype.load

mod.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;
};

调用每个依赖模块的 exec 方法。如果 seajs.use 传入了 callback, 那么会传入 exports 的对象为 callback 的参数。删除 callback, history, remain, _entry 属性,减少内存。

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道