Sea.js是如何工作的?

2020-04-26 10:18 更新

蒙惠者虽知其然,而未必知其所以然也。

写了这么多,必须证明一下本书并不是一份乏味的使用文档,我们来深入看看Sea.js,搞清楚它时如何工作的吧!

CMD规范

要想了解Sea.js的运作机制,就不得不先了解其CMD规范。

Sea.js采用了和Node相似的CMD规范,我觉得它们应该是一样的。使用require、exports和module来组织模块。但Sea.js比起Node的不同点在于,前者的运行环境是在浏览器中,这就导致A依赖的B模块不能同步地读取过来,所以Sea.js比起Node,除了运行之外,还提供了两个额外的东西:

  1. 模块的管理
  2. 模块从服务端的同步

即Sea.js必须分为模块加载期和执行期。加载期需要将执行期所有用到的模块从服务端同步过来,在再执行期按照代码的逻辑顺序解析执行模块。本身执行期与node的运行期没什么区别。

所以Sea.js需要三个接口:

  1. define用来wrapper模块,指明依赖,同步依赖;
  2. use用来启动加载期;
  3. require关键字,实际上是执行期的桥梁。

并不太喜欢Sea.js的use API,因为其回调函数并没有使用与Define一样的参数列表。

模块标识(id)

模块id的标准参考Module Identifiers,简单说来就是作为一个模块的唯一标识。

出于学习的目的,我将它们翻译引用在这里:

  1. 模块标识由数个被斜杠(/)隔开的词项组成;
  2. 每次词项必须是小写的标识、“.”或“..”;
  3. 模块标识并不是必须有像“.js”这样的文件扩展名;
  4. 模块标识不是相对的,就是顶级的。相对的模块标识开头要么是“.”,要么是“..”;
  5. 顶级标识根据模块系统的基础路径来解析;
  6. 相对的模块标识被解释为相对于某模块的标识,“require”语句是写在这个模块中,并在这个模块中调用的。

模块(factory)

顾名思义,factory就是工厂,一个可以产生模块的工厂。node中的工厂就是新的运行时,而在Sea.js中(Tea.js中也同样),factory就是一个函数。这个函数接受三个参数。

function (require, exports, module) {
    // here is module body
}

在整个运行时中只有模块,即只有factory。

依赖(dependencies)

依赖就是一个id的数组,即模块所依赖模块的标识。

依赖加载的原理

有很多语言都有模块化的结构,比如c/c++的#include语句,Ruby的require语句等等。模块的执行,必然需要其依赖的模块准备就绪才能顺利执行。

c/c++是编译语言,在预编译时,替换#include语句,将依赖的文件内容包含进来,在编译后的执行期,所有的模块才会开始执行;

而Ruby是解释型语言,在模块执行前,并不知道它依赖什么模块,待到执行到require语句时,执行将暂停,从外部读取并执行依赖,然后再回来继续执行当前模块。

JavaScript作为一门解释型语言,在复杂的浏览器环境中,Sea.js是如何处理CMD模块间的依赖的呢?

node的方式-同步的require

想要解释这个问题,我们还是从Node模块说起,node于Ruby类似,用我们之前使用过的一个模块作为例子:

// File: usegreet.js
var greet = require("./greet");
greet.helloJavaScript();

当我们使用node usegreet.js来运行这个模块时,实际上node会构建一个运行的上下文,在这个上下文中运行这个模块。运行到require('./greet')这句话时,会通过注入的API,在新的上下文中解析greet.js这个模块,然后通过注入的exportsmodule这两个关键字获取该模块的接口,将接口暴露出来给usegreet.js使用,即通过greet这个对象来引用这些接口。例如,helloJavaScript这个函数。详细细节可以参看node源码中的 module.js

node的模块方案的特点如下:

  1. 使用require、exports和module作为模块化组织的关键字;
  2. 每个模块只加载一次,作为单例存在于内存中,每次require时使用的是它的接口;
  3. require是同步的,通俗地讲,就是node运行A模块,发现需要B模块,会停止运行A模块,把B模块加载好,获取的B的接口,才继续运行A模块。如果B模块已经加载到内存中了,当然require B可以直接使用B的接口,否则会通过fs模块化同步地将B文件内存,开启新的上下文解析B模块,获取B的API。

实际上node如果通过fs异步的读取文件的话,require也可以是异步的,所以曾经node中有require.async这个API。

Sea.js的方式-加载期与执行期

由于在浏览器端,采用与node同样的依赖加载方式是不可行的,因为依赖只有在执行期才能知道,但是此时在浏览器端,我们无法像node一样直接同步地读取一个依赖文件并执行!我们只能采用异步的方式。于是Sea.js的做法是,分成两个时期——加载期和执行期;

的确,我们可以使用同步的XHR从服务端加载依赖,但是本身就是单进程的JavaScript还需要等待文件的加载,那性能将大打折扣。

  • 加载期:即在执行一个模块之前,将其直接或间接依赖的模块从服务器端同步到浏览器端;
  • 执行期:在确认该模块直接或间接依赖的模块都加载完毕之后,执行该模块。

加载期

不难想见,模块间的依赖就像一棵树。启动模块作为根节点,依赖模块作为叶子节点。下面是pixelegos的依赖树:


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号