详细解析 Sea.js 的模块依赖加载与执行机制。本文直接切入核心,梳理其关键逻辑的运行流程。
入口方法
每个模块系统都存在一个入口,类似于 C 语言中的 main 函数。在 Sea.js 中,这个入口由 seajs.use() 承担。它接收两个参数:模块名称列表和回调函数。入口方法内部会创建一个新模块,该模块依赖于传入的所有模块,并设置一个回调,当所有依赖进入 loaded 状态后触发执行。回调的主要任务是依次调用每个依赖模块的工厂函数,最后再运行入口方法中用户提供的那个回调。
// Public API
// 入口地址
seajs.use = function(ids, callback) {
Module.preload(function() {
Module.use(ids, callback, data.cwd + "_use_" + cid())
})
return seajs
}
// Load preload modules before all other modules
Module.preload = function(callback) {
var preloadMods = data.preload
var len = preloadMods.length
if (len) {
Module.use(preloadMods, function() {
// Remove the loaded preload modules
preloadMods.splice(0, len)
// Allow preload modules to add new preload modules
Module.preload(callback)
}, data.cwd + "_preload_" + cid())
}
else {
callback()
}
}
// 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.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.preload 主要用于 Sea.js 插件体系,并非核心路径,可以暂时忽略。Module.use 才是真正的核心入口方法:创建匿名模块,绑定回调,然后触发其所有依赖的加载流程。
加载依赖之 load 方法
load 方法是 Sea.js 最为精妙的部分。它负责递归加载依赖模块,并按照依赖关系依次触发回调,最终会执行到通过 seajs.use 创建的匿名模块的回调(即 mod.callback)。
该方法通过递归实现依赖加载:如果依赖模块本身还有子依赖,则继续深入加载。关键在于两个实例属性:_waitings 和 _remain。
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, mod)
var len = mod._remain = uris.length
var m
// Initialize modules and register waitings
for (var i = 0; i < len; i++) {
m = Module.get(uris[i])
// 修改 依赖文件 的 _waiting属性
if (m.status < STATUS.LOADED) {
// Maybe duplicate: When module has dupliate dependency, it should be it's count, not 1
m._waitings[mod.uri] = (m._waitings[mod.uri] || 0) + 1
}
else {
mod._remain--
}
}
// 加载完依赖,执行模块
if (mod._remain === 0) {
mod.onload()
return
}
// Begin parallel loading
var requestCache = {}
for (i = 0; i < len; i++) {
m = cachedMods[uris[i]]
// 该依赖并未加载,则先fetch,将seajs.request函数绑定在对应的requestCache上,此时并未加载模块
if (m.status < STATUS.FETCHING) {
m.fetch(requestCache)
}
else if (m.status === STATUS.SA VED) {
m.load()
}
}
// Send all requests at last to a void cache bug in IE6-9. Issues#808
// 加载所有模块
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
// 此时加载模块
requestCache[requestUri]()
}
}
}
// 依赖模块加载完毕执行回调函数
// 并检查依赖该模块的其他模块是否可以执行
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
if (mod.callback) {
mod.callback()
}
console.log(mod)
// Notify waiting modules to fire 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()
}
}
}
// Reduce memory taken
delete mod._waitings
delete mod._remain
}
执行流程如下:首先初始化 _waitings 和 _remain。如果 _remain 为 0,说明没有依赖或依赖已经加载就绪,直接调用 onload;否则,将尚未加载的模块通过 fetch 拉取下来。
实现中有一个巧妙的小技巧:所有加载请求先统一收集到 requestCache 对象中,最后集中发出,从而实现并行加载效果。在 fetch 函数中会进行如下绑定:
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
其中 sendRequest 最终调用 seajs.request:
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
}
所有依赖并行加载完成后,会执行 onRequest 回调,继续向上冒泡加载更深的依赖层,直到到达最顶层模块。
当最上层模块的所有子依赖都已加载完毕时,执行 onload 函数:将模块状态设为 loaded,执行 mod.callback,然后检查并更新依赖它的各模块的 _waitings 计数。如果下层模块也已无剩余依赖,则继续触发下层模块的 mod.callback。这样逐层回溯,最终会执行到由 seajs.use 创建的匿名模块的回调。
例证
用一个简单实例验证上述流程:
tst.html
-------------------------------------
a.js
define(function(require,exports,module){
exports.add = function(a,b){
return a+b;
}
})
------------------------------------
b.js
define(function(require,exports,module){
var a = require("./a");
console.log(a.add(3,5));
})
通过调试工具可以观察到 onload 的执行顺序:

最顶层的匿名模块状态为 4(即未执行),因为它没有工厂函数,确实无法执行。
模块执行之 exec
模块的实际执行发生在 seajs.use 所定义的 mod.callback 内部,该回调会依次调用所有依赖模块的 exec 方法。在 exec 方法中,会创建 CommonJS 环境下的 require、exports 等关键对象:
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 a voiding
// circularly calling
if (mod.status >= STATUS.EXECUTING) {
return mod.exports
}
mod.status = STATUS.EXECUTING
// Create require
var uri = mod.uri
function require(id) {
return Module.get(require.resolve(id)).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
// 工厂函数有返回值,则返回;
// 无返回值,则返回mod.exports
var exports = isFunction(factory) ?
factory(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 exports
}
require 函数用于获取指定模块并执行其工厂函数,返回导出的值。require.resolve 负责将模块标识解析为绝对路径。require.async 则提供异步加载依赖并执行回调的能力。
关于工厂函数的返回值处理:如果工厂函数本身是一个对象,那么该对象直接作为 exports;如果工厂函数有返回值,则采用返回值作为 exports;否则将 module.exports 作为 exports 返回。
一个值得注意的细节: 如果试图通过直接给 exports 赋值来导出对象:
define(function(require,exports,module){
exports ={
add: function(a,b){
return a+b;
}
}
})
这种做法不会生效。因为 JavaScript 的参数传递遵循“按共享传递”,初始时 exports === module.exports,但当为 exports 赋予一个新对象时,exports 便指向该新对象,而 module.exports 仍为 undefined,最终导出的将是 undefined。
正确的做法是:
define(function(require,exports,module){
module.exports ={
add: function(a,b){
return a+b;
}
}
})
总结
至此,Sea.js 核心模块的实现已分析完毕。源码中蕴含了许多精巧的编码技巧,回调模式运用得极为灵活,同时对内存泄漏、this 指针偏移等问题都做了积极的防御处理。这种设计哲学非常值得借鉴。
阅读 Sea.js 源码花费了一周多的时间,从最初的懵懂到逐渐透彻,真切体会到设计思想的重要性。过去可能更关注“能跑起来、不出 bug、有一定健壮性”,但深入 load 依赖模块的实现后,才明白技巧性往往决定了代码的美感与可维护性。希望这篇文章能对你有所启发。
