Sea.Js的运行原理
1.CMD(Common Module Definition)规范
Sea.js采用了和Node相似的CMD规范,使用require、exports和module来组织模块。但Sea.js比起Node的不同点在于,前者的运行环境是在浏览器中,这就导致A依赖的B模块不能同步地读取过来,所以Sea.js比起Node,除了运行之外,还提供了两个额外的东西:
a.模块的管理
b.模块从服务端的同步
即Sea.js必须分为模块加载期和执行期。加载期需要将执行期所有用到的模块从服务端同步过来,在再执行期按照代码的逻辑顺序解析执行模块。本身执行期与node的运行期没什么区别。
Sea.js需要三个接口:define 用来wapper模块,指明依赖,同步依赖;use 用来启动加载期;require 实际上是加载期到执行期的桥梁。
模块标识:模块id的标准参考Module Identifiers,简单说来就是作为一个模块的唯一标识。
Factory:一个可以产生模块的工厂。node中的工厂就是新的运行时,而在Sea.js中(Tea.js中也同样),factory就是一个函数。这个函数接受三个参数。
依赖(Dependencies):依赖就是一个id的数组,即模块所依赖模块的标识
2.依赖加载原理
有很多语言都有模块化的结构,比如c/c++的#include语句,Ruby的require语句等等。模块的执行,必然需要其依赖的模块准备就绪才能顺利执行。
c/c++是编译语言,在预编译时,替换#include语句,将依赖的文件内容包含进来,在编译后的执行期,所有的模块才会开始执行;
而Ruby是解释型语言,在模块执行前,并不知道它依赖什么模块,待到执行到require语句时,执行将暂停,从外部读取并执行依赖,然后再回来继续执行当前模块。
JavaScript作为一门解释型语言,在复杂的浏览器环境中,Sea.js是如何处理CMD模块间的依赖的呢?
a.Node的依赖加载原理
node于Ruby类似,当我们使用node usegreet.js来运行这个模块时,实际上node会构建一个运行的上下文,在这个上下文中运行这个模块。运行到require(‘./greet‘)这句话时,会通过注入的API,在新的上下文中解析greet.js这个模块,然后通过注入的exports或module这两个关键字获取该模块的接口,将接口暴露出来给usegreet.js使用,即通过greet这个对象来引用这些接口。
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。
b.Sea.js加载原理
由于在浏览器端,采用与node同样的依赖加载方式是不可行的,因为依赖只有在执行期才能知道,但是此时在浏览器端,我们无法像node一样直接同步地读取一个依赖文件并执行!我们只能采用异步的方式。于是Sea.js的做法是,分成两个时期——加载期和执行期;
加载期:即在执行一个模块之前,将其直接或间接依赖的模块从服务器端同步到浏览器端;
执行期:在确认该模块直接或间接依赖的模块都加载完毕之后,执行该模块。
加载期:不难想见,模块间的依赖就像一棵树。启动模块作为根节点,依赖模块作为叶子节点。下面是pixelegos(一个开源项目)的依赖树:
如上图,在页面中通过seajs.use(‘/js/pixelegos‘)
调用,目的是执行pixelegos这个模块。Sea.js并不知道pixelegos还依赖于其他什么模块,只是到服务端加载pixelegos.js,将其加载到浏览器端之后,通过分析发现它还依赖于其他的模块,于是Sea.js又去加载其他的模块。随着更多的模块同步到浏览器端后,一棵依赖树才慢慢地通过递归显现出来。
执行期:在执行期,执行也是从根节点开始,本质上是按照代码的顺序结构,对整棵树进行了遍历。有的模块可能已经EXECUTED,而有的还需要执行获取其exports。由于在执行期时,所有依赖的模块都加载好了,所以与node执行过程有点类似。
pixelegos通过同步的require函数获取tool、canvas和menu,后三者同样通过require来执行各自的依赖模块,于是通过这样一个递归的过程,pixelegos就执行完毕了。
打包模块的加载过程:
打包的方式有三种,self,relative和all。
self,只是自己做了transport
relative,将多有相对路径的模块transport,concat
all,包括相对路径模块和库模块(即在seajs-modules文件夹中的),transport,concat
加载方式(以压缩的pixelegos.js为例)
(1)在use时,定义一个匿名的use_模块,依赖于/dist/pixelegos模块,匿名的use_模块load依赖,开始加载pixelegos.js模块;
(2)pixelegos.js加载执行,所有打包在里面的模块被define;
(3)pixelegos.js的onload回调执行,调用/dist/pixelegos模块的load,加载其依赖模块,但依赖的模块都加载好了;
(4)通知匿名的use_加载完成,开始执行期。
3.Sea.js的实现
module.js是Sea.js的核心,Sea.js中为模块定义了六种状态:
FETCHING:开始从服务端加载模块
SAVED:模块加载完成
LOADING:加载依赖模块中
LOADED:依赖模块加载完成
EXECUTING:模块执行中
EXECUTED:模块执行完成
Sea.use调用Module.use构造一个没有factory的模块,该模块即为这个运行期的根节点
// 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() }
模块构造完成,则调用mod.load()来同步其子模块;直接跳过fetching这一步;mod.callback也是Sea.js不纯粹的一点,在模块加载完成后,会调用这个callback。
在load方法中,获取子模块,加载子模块,在子模块加载完成后,会触发mod.onload():
// 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) 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]) if (m.status < STATUS.LOADED) { // Maybe duplicate 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]] 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]() } } }
模块的状态是最关键的,模块状态的流转决定了加载的行为;
是否触发onload是由模块的_remian属性来确定,在load和子模块的onload函数中都对_remain进行了计算,如果为0,则表示模块加载完成,调用onload:
// Call this method when module is loaded Module.prototype.onload = function () { var mod = this mod.status = STATUS.LOADED if (mod.callback) { mod.callback() } // 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 }
模块的_remain和_waitings是两个非常关键的属性,子模块通过_waitings获得父模块,通过_remain来判断模块是否加载完成。
当这个没有factory的根模块触发onload之后,会调用其方法callback,callback是这样的:
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 }
这预示着加载期结束,开始执行期;
而执行期相对比较无脑,首先是直接调用根模块依赖模块的exec方法获取其exports,用它们来调用use传经来的callback。而子模块在执行时,都是按照标准的模块解析方式执行的:
// Execute a module 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 // 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 var exports = isFunction(factory) ? factory(require, mod.exports = {}, mod) : factory if (exports === undefined) { exports = mod.exports } // Emit `error` event if (exports === null && ! IS_CSS_RE.test(uri)) { emit("error", mod) } // Reduce memory leak delete mod.factory mod.exports = exports mod.status = STATUS.EXECUTED // Emit `exec` event emit("exec", mod) return exports }
注意:var exports = isFunction(factory) ? factory(require, mod.exports = {}, mod) : factory
真的,整个Sea.js就是为了这行代码能够完美运行
4.资源定位
资源定位与模块标识相关,而在Sea.js中有三种模块标识:
普通路径:普通路径与网页中超链接一样,相对于当前页面解析。
相对标识:在define的factory中的相对路径(..
.
)是相对标识,相对标识相对当前的URI来解析
顶级标识:不以.
或者‘/‘开头的模块标识是顶级标识。
获取真实路径:在Sea.js中,使用data.cwd来代表当前页面的目录;使用data.base来代表sea.js的加载地址。
5.factory依赖分析
在Sea.js的API中,define(factory)
,并没有指明模块的依赖项,那Sea.js是如何获得的呢。
/** * util-deps.js - The parser for dependencies * ref: tests/research/parse-dependencies/test.html */ var REQUIRE_RE = /"(?:\\"|[^"])*"|‘(?:\\‘|[^‘])*‘|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["‘])(.+?)\1\s*\)/g var SLASH_RE = /\\\\/g function parseDependencies(code) { var ret = [] code.replace(SLASH_RE, "") .replace(REQUIRE_RE, function(m, m1, m2) { if (m2) { ret.push(m2) } }) return ret }
Sea.js就是使用REQUIRE_RE
在factory的源码中匹配出该模块的依赖项。从REQUIRE_RE这么长的正则来看,这里坑很多;在CommonJS的wrapper方案中可以使用JS语法分析器来获取依赖会更准确。