游乐游手机版
首页/前端开发/文章详情

SeaJS模块依赖加载与模块执行流程

时间:2026-06-10 06:57
Sea js通过seajs use创建匿名模块,调用Module use加载依赖,用load递归加载并借助_waitings和_remain追踪状态,并行请求,依赖全部加载后执行回调,通过exec获取模块导出。

详细解析 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 环境下的 requireexports 等关键对象:

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

这种做法不会生效。因为 Java​​Script 的参数传递遵循“按共享传递”,初始时 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 依赖模块的实现后,才明白技巧性往往决定了代码的美感与可维护性。希望这篇文章能对你有所启发。

来源:https://www.jb51.net/article/95306.htm
上一篇SeaJS模块依赖加载与API导出详解 下一篇Sea.js模块加载器常用API快速实用简易文档速查手册
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
HTML文档版权声明结构与基础脚手架代码质量解析
前端开发 · 2026-07-07

HTML文档版权声明结构与基础脚手架代码质量解析

版权声明需用语义化``标签,©符号使用`©`实体;年份通过服务端、构建或JS分层兜底;`rel= "license "`仅指向真实许可证文件才有效。

基于HTML模板的组件化全栈交互方案
前端开发 · 2026-07-07

基于HTML模板的组件化全栈交互方案

纯HTML模板仅作静态结构,全栈交互需后端注入数据、前端JS激活模板及用户事件触发双向链路。``标签惰性,不执行脚本或加载资源,需通过克隆填充数据并手动绑定事件实现交互,父子传值依赖JS对象与自定义事件。

Bootstrap修改Badge徽章边框为圆点样式的方法
前端开发 · 2026-07-07

Bootstrap修改Badge徽章边框为圆点样式的方法

将BootstrapBadge改为圆点:自定义类设置等宽高、border-radius:50%、padding:0、font-size:0;Bootstrap5 2+需加display:inline-block;注意垂直对齐,避免min-width干扰,背景色用background-color更可控。

Bootstrap响应式多栏Footer社交图标排版实现
前端开发 · 2026-07-07

Bootstrap响应式多栏Footer社交图标排版实现

使用Bootstrap5的grid系统实现响应式多栏Footer,按断点设置列宽;以FontAwesome处理社交图标,避免使用img;利用list-unstyled组织链接列表;借助flex-column容器配合mt-auto使版权行自动贴底,避免fixed-bottom固定定位的弊端。

如何用Shadow DOM隔离组件内外事件分发
前端开发 · 2026-07-07

如何用Shadow DOM隔离组件内外事件分发

ShadowDOM内部事件默认不穿透影子边界,需显式设置composed:true才能被外部监听。此时event target指向宿主元素,真实触发源需通过event composedPath()[0]获取。原生事件手动派发时也必须声明composed:true。应避免在宿主上依赖event target做逻辑分支,建议在shadowroot内绑定事件或使用