seajs模块加载机制

96
江枫
2014.11.24 22:29* 字数 1348

本文谈论的代码版本是Sea.js 2.3.0,seajs最新的版本是3.0,3.0版本变动较大.

新事物的出现或多或少的是想改变已有的行为方式,解决遇到的问题。关于这段历史,大家可以看看这几篇文章:why-seajs1why-seajs2

关于seajs原理介绍和分析的文字也有不少,例如:Sea.js是如何工作的?实例解析 SeaJS 内部执行过程 - 从 use 说起、也有人分析了seajs代码中的数据结构:SeaJSV1.1.0-代码结构和数据结构


既然已经有很多优秀的文章介绍seajs了,为什么你还要浪费时间呢?

因为脑子不好使,写作的过程使我审视自己的结论,加深了我对事物的理解。

seajs干嘛的

SeaJS是一个遵循CMD规范的JavaScript模块加载框架,用以实现JavaScript的模块化开发及加载机制

注意关键词:CommonJS规范,模块加载,模块化开发

  • CMD规范全称Common Module Definition,该规范明确了模块的基本书写格式和基本交互规则,即定义了一组模块化开发的接口,具体细节可以参看:CMD 模块定义规范英文 Common Module Definition

  • 有C++,Java,Python开发经验的同学应该很容易理解模块化开发和模块加载的概念,以java为例,类是java的最小开发单位,一个类可以看做是一个最小模块,一个或多个类可以组成一个package,这种组织代码的方式就是模块化开发

  • 当我们在一个类中import另一个package的内容时,import的内容最终会被包含进当前类中,seajs要做的一个事情就是模拟java中import这种模块加载机制。

  • 与服务器端不同,在浏览器端实现模块加载又有其特点:模块(js文件)需要经过网络传输到达用户浏览器,那么何时将js文件下载到用户本地?性能考虑,提前将用到的js文件下载到用户本地是首选,免去了执行期从服务器请求文件消耗的时间,但同时seajs也提供了异步加载模块的功能。

两个核心

1. 模块的依赖加载

如何从服务器获取js文件,这里就不详细解释了,原理就是append script标签的方式。

模块间的关系无非就是依赖和被依赖的关系,具体到两个模块上,有下图三种关系:

  1. a模块依赖b模块
  2. b模块依赖a模块
  3. a,b模块相互依赖
两个模块间的依赖关系

前两种情况很好理解,对于第三种情况,seajs在3.0版本前是不支持循环依赖的,具体表现为模块加载会中断,a.doSomething()并没有执行,3.0的循环依赖处理和node一样了#1382,下面的代码可以用来测试这个问题。

seajs.use("a" , function(a){
    a.doSomething();
});

//a.js
define(function(require, exports , module) {
    var b = require("b");
    exports.doSomething = function() {
        console.log("in a");
    };
});

//b.js
define(function(require, exports, module) {
    var c = require("c");
    exports.doSomething = function() {
        console.log("in b");
    };
});

//c.js
define(function(require, exports, module) {
    var a = require("a");
    exports.doSomething = function() {
        console.log("in c");
    };
});

提炼一下上面描述的内容就是:对于一个模块M,它应该拥有下面的关系

Module{
a: {Module依赖的模块}
b: {依赖Module的模块}
}

在看看seajs中Module的定义:

function Module(uri, deps) {
  this.uri = uri
  this.dependencies = deps || []//我依赖的模块
  this.exports = null
  this.status = 0   //我当前的状态
  this._waitings = {} //依赖我的模块
  this._remain = 0  //我依赖的模块还有多少没有完成加载
}

以一个实际的例子说明一下seajs模块加载的逻辑,如下图:

百度脑图
  • a依赖b和c
  • b依赖d

尽管存在上述依赖,但是a,b,c,d模块download到浏览器端的顺序确是a,b,c,d,而不是d,b,c,a,笨想一下后一种执行顺序也是不可能的,因为模块间的依赖只有download到浏览器端seajs才能进行分析。

概括一下整个加载的流程就是:

自顶向下的download,自底向上的反馈准备就绪。

如何做到的呢?

  1. 主要是Module中的几个属性发挥的作用,模块被download到浏览器端后,按照CMD规范,define函数会被执行,module.define会分析该模块的依赖,记录到dependencies属性中,define函数执行完毕,绑定在script标签上的onload事件会被触发,进而加载当前模块的依赖模块,也就是执行module.load函数,这是一个循环往复的过程。

  2. 假设d模块加载就绪,执行module.load时发现,d模块已无其他依赖,进而执行module.onload, 在module.onload中,通过_waitings属性找到父模块,操作父模块的依赖计数_remain,达到通知父模块的目的。

这是一个完美的反馈系统。

seajs的模块加载可以分为两部分,一部分就是模块加载的过程,另一部分就是模块执行的过程。相比较而言,执行过程就比较简单了,具体可以看源码module.exec这个函数。

下一节分析seajs中模块id的解析原理。

js
Web note ad 1