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语法分析器来获取依赖会更准确。
Sea.Js的运行原理(转)的更多相关文章
- js 网页运行原理
当我们打开一个网页的时候,浏览器会首先创建一个窗口,这个窗口就是我所知道的window对象,也就是整个Javascript运行所依附的全局变量. 为了加载网页文档,当前窗口又需要创建一个Documen ...
- Js基础知识(四) - js运行原理与机制
js运行机制 本章了解一下js的运行原理,了解了js的运行原理才能写出更优美的代码,提高运行效率,还能解决开发中遇到的不理解的问题. 进程与线程 进程是cpu资源分配的最小单位,进程可以包含多个线程. ...
- Sea.js提供简单、极致的模块化开发体验
为什么使用 Sea.js ? Sea.js 追求简单.自然的代码书写和组织方式,具有以下核心特性: 简单友好的模块定义规范:Sea.js 遵循 CMD 规范,可以像 Node.js 一般书写模块代码. ...
- Sea.Js使用入门
1.Sea.Js是什么 seajs相对于RequireJs与LabJS就比较年轻,2010年玉伯发起了这个开源项目,SeaJS遵循CMD规范,与RequireJS类似,同样做为模块加载器.示例 // ...
- sea.js的模块化开发
为什么使用sea.js? Sea.js 追求简单.自然的代码书写和组织方式,具有以下核心特性: 简单友好的模块定义规范:Sea.js 遵循 CMD 规范,可以像Node.js 一般书写模块代码. 自然 ...
- 第三课:sea.js模块加载原理
模块加载,其实就是把js分成很多个模块,便于开发和维护.因此加载很多js模块的时候,需要动态的加载,以便提高用户体验. 在介绍模块加载库之前,先介绍一个方法. 动态加载js方法: function l ...
- 模块化开发之sea.js实现原理总结
seajs官网说:seajs是一个模块加载器,所以学习它并不难. 在我的理解就是:本来我们是需要手动创建 script标签 引入 js文件的,但用seajs后,它就自动帮我们完成这些工作. 这里只说实 ...
- 【原创】分布式之数据库和缓存双写一致性方案解析(三) 前端面试送命题(二)-callback,promise,generator,async-await JS的进阶技巧 前端面试送命题(一)-JS三座大山 Nodejs的运行原理-科普篇 优化设计提高sql类数据库的性能 简单理解token机制
[原创]分布式之数据库和缓存双写一致性方案解析(三) 正文 博主本来觉得,<分布式之数据库和缓存双写一致性方案解析>,一文已经十分清晰.然而这一两天,有人在微信上私聊我,觉得应该要采用 ...
- 前端与编译原理 用js去运行js代码 js2run
# 前端与编译原理 用js去运行js代码 js2run 前端与编译原理似乎相隔甚远,各种热门的框架都学不过来,那能顾及到这么多底层呢,前端开发者们似乎对编译原理的影响仅仅是"抽象语法树&qu ...
随机推荐
- Openjudge-计算概论(A)-能被3,5,7整除的数
描述: 输入一个整数,判断它能否被3,5,7整除,并输出以下信息:1.能同时被3,5,7整除(直接输出3 5 7,每个数中间一个空格):2.能被其中两个数整除(输出两个数,小的在前,大的在后.例如:3 ...
- 正则表达式 Pattern & Matcher
1 compile and pattern Pattern类用于创建一个正则表达式,也可以说创建一个匹配模式,它的构造方法是私有的,不可以直接创建,但可以通过Pattern.complie(Strin ...
- sha加密算法
密钥生成 公钥(e,n) 私钥(d,n) 找两个互质的大素数p和q, 计算n=p*p, f(n)=(p-1)*(q-1) 选择随机整数e(e和f(n)互质) de=f(n)mod 1 利用公钥加密 ...
- jsp之用户自定义标签
创建一个类,引入外部jsp-api.jar包(在tomcat 下lib包里有),这个类继承SimpleTagSupport 重写doTag()方法. jspprojec包下的helloTag类: pu ...
- 在TextView上显示图片信息
布局文件 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:t ...
- iOS拨打电话
1,这种方法,拨打完电话回不到原来的应用,会停留在通讯录里,而且是直接拨打,不弹出提示NSMutableString * str=[[NSMutableString alloc] initWithFo ...
- php项目第三季
1.百度浏览器路径写相对路径 2.svn--setting中两个勾去掉,第二页一个勾打上.工程上右击update是更新,commit是提交. 3.Deprecated: mysql_connect() ...
- 错误记录-spring+mybatis
1.Syntax error on token "String", @ expected 解决:去掉类名后的括号 ps:这错误太二了 2.The nested type UserS ...
- Web开发人员不要错过的60款用户界面设计工具(下)
41. Snipplr 包含最新的脚本和jQuery技术资源库. 42. Midori Midori是一个超轻量级的JavaScript框架,可使用CSS选择器快速访问页面上对应的元素. 43. ro ...
- 开源日志系统比较:scribe、chukwa、kafka、flume
1. 背景介绍 许多公司的平台每天会产生大量的日志(一般为流式数据,如,搜索引擎的pv,查询等),处理这些日志需要特定的日志系统,一般而言,这些系统需要具有以下特征: (1) 构建应用系统和分析系统的 ...