蒙惠者雖知其然,而未必知其所以然也。
寫了這么多,必須證明一下本書并不是一份乏味的使用文檔,我們來深入看看Sea.js,搞清楚它時(shí)如何工作的吧!
要想了解Sea.js的運(yùn)作機(jī)制,就不得不先了解其CMD規(guī)范。
Sea.js采用了和Node相似的CMD規(guī)范,我覺得它們應(yīng)該是一樣的。使用require、exports和module來組織模塊。但Sea.js比起Node的不同點(diǎn)在于,前者的運(yùn)行環(huán)境是在瀏覽器中,這就導(dǎo)致A依賴的B模塊不能同步地讀取過來,所以Sea.js比起Node,除了運(yùn)行之外,還提供了兩個(gè)額外的東西:
即Sea.js必須分為模塊加載期和執(zhí)行期。加載期需要將執(zhí)行期所有用到的模塊從服務(wù)端同步過來,在再執(zhí)行期按照代碼的邏輯順序解析執(zhí)行模塊。本身執(zhí)行期與node的運(yùn)行期沒什么區(qū)別。
所以Sea.js需要三個(gè)接口:
并不太喜歡Sea.js的use API,因?yàn)槠浠卣{(diào)函數(shù)并沒有使用與Define一樣的參數(shù)列表。
模塊id的標(biāo)準(zhǔn)參考Module Identifiers,簡(jiǎn)單說來就是作為一個(gè)模塊的唯一標(biāo)識(shí)。
出于學(xué)習(xí)的目的,我將它們翻譯引用在這里:
顧名思義,factory就是工廠,一個(gè)可以產(chǎn)生模塊的工廠。node中的工廠就是新的運(yùn)行時(shí),而在Sea.js中(Tea.js中也同樣),factory就是一個(gè)函數(shù)。這個(gè)函數(shù)接受三個(gè)參數(shù)。
function (require, exports, module) {
// here is module body
}
在整個(gè)運(yùn)行時(shí)中只有模塊,即只有factory。
依賴就是一個(gè)id的數(shù)組,即模塊所依賴模塊的標(biāo)識(shí)。
有很多語(yǔ)言都有模塊化的結(jié)構(gòu),比如c/c++的#include
語(yǔ)句,Ruby的require
語(yǔ)句等等。模塊的執(zhí)行,必然需要其依賴的模塊準(zhǔn)備就緒才能順利執(zhí)行。
c/c++是編譯語(yǔ)言,在預(yù)編譯時(shí),替換#include
語(yǔ)句,將依賴的文件內(nèi)容包含進(jìn)來,在編譯后的執(zhí)行期,所有的模塊才會(huì)開始執(zhí)行;
而Ruby是解釋型語(yǔ)言,在模塊執(zhí)行前,并不知道它依賴什么模塊,待到執(zhí)行到require
語(yǔ)句時(shí),執(zhí)行將暫停,從外部讀取并執(zhí)行依賴,然后再回來繼續(xù)執(zhí)行當(dāng)前模塊。
JavaScript作為一門解釋型語(yǔ)言,在復(fù)雜的瀏覽器環(huán)境中,Sea.js是如何處理CMD模塊間的依賴的呢?
require
想要解釋這個(gè)問題,我們還是從Node模塊說起,node于Ruby類似,用我們之前使用過的一個(gè)模塊作為例子:
// File: usegreet.js
var greet = require("./greet");
greet.helloJavaScript();
當(dāng)我們使用node usegreet.js
來運(yùn)行這個(gè)模塊時(shí),實(shí)際上node會(huì)構(gòu)建一個(gè)運(yùn)行的上下文,在這個(gè)上下文中運(yùn)行這個(gè)模塊。運(yùn)行到require('./greet')
這句話時(shí),會(huì)通過注入的API,在新的上下文中解析greet.js這個(gè)模塊,然后通過注入的exports
或module
這兩個(gè)關(guān)鍵字獲取該模塊的接口,將接口暴露出來給usegreet.js使用,即通過greet
這個(gè)對(duì)象來引用這些接口。例如,helloJavaScript
這個(gè)函數(shù)。詳細(xì)細(xì)節(jié)可以參看node源碼中的module.js。
node的模塊方案的特點(diǎn)如下:
實(shí)際上node如果通過fs異步的讀取文件的話,require也可以是異步的,所以曾經(jīng)node中有require.async這個(gè)API。
由于在瀏覽器端,采用與node同樣的依賴加載方式是不可行的,因?yàn)橐蕾囍挥性趫?zhí)行期才能知道,但是此時(shí)在瀏覽器端,我們無(wú)法像node一樣直接同步地讀取一個(gè)依賴文件并執(zhí)行!我們只能采用異步的方式。于是Sea.js的做法是,分成兩個(gè)時(shí)期——加載期和執(zhí)行期;
的確,我們可以使用同步的XHR從服務(wù)端加載依賴,但是本身就是單進(jìn)程的JavaScript還需要等待文件的加載,那性能將大打折扣。
不難想見,模塊間的依賴就像一棵樹。啟動(dòng)模塊作為根節(jié)點(diǎn),依賴模塊作為葉子節(jié)點(diǎn)。下面是pixelegos的依賴樹:
如上圖,在頁(yè)面中通過seajs.use('/js/pixelegos')
調(diào)用,目的是執(zhí)行pixelegos這個(gè)模塊。Sea.js并不知道pixelegos還依賴于其他什么模塊,只是到服務(wù)端加載pixelegos.js,將其加載到瀏覽器端之后,通過分析發(fā)現(xiàn)它還依賴于其他的模塊,于是Sea.js又去加載其他的模塊。隨著更多的模塊同步到瀏覽器端后,一棵依賴樹才慢慢地通過遞歸顯現(xiàn)出來。
那Sea.js如何確定pixelegos所有依賴的模塊都加載好了呢?
從依賴樹中可以看出,如果pixelegos.js所依賴的直接子節(jié)點(diǎn)加載好了(此種加載好,即為節(jié)點(diǎn)和其依賴的子節(jié)點(diǎn)都加載好),那就表示它就加載好了,于是就可以啟動(dòng)該模塊。明顯,這種加載完成的過程也是一個(gè)遞歸的過程。
從最底層的葉子節(jié)點(diǎn)開始(例如undercore),由于沒有再依賴于其他模塊,所以它從服務(wù)端同步過來之后,就加載好了。然后開始詢問其父節(jié)點(diǎn)backbone是否已經(jīng)加載好了,即詢問backbone所依賴的所有節(jié)點(diǎn)都加載好了。同理對(duì)于pixelegos模塊,其子節(jié)點(diǎn)menu、tool、canvas都會(huì)詢問pixelegos其子節(jié)點(diǎn)加載好了沒有。
如果三個(gè)依賴都已loading完畢,則pixelgos也加載完成,即其整棵依賴樹都加載好了,然后就可以啟動(dòng)pixelegos這個(gè)模塊了。
在執(zhí)行期,執(zhí)行也是從根節(jié)點(diǎn)開始,本質(zhì)上是按照代碼的順序結(jié)構(gòu),對(duì)整棵樹進(jìn)行了遍歷。有的模塊可能已經(jīng)EXECUTED,而有的還需要執(zhí)行獲取其exports。由于在執(zhí)行期時(shí),所有依賴的模塊都加載好了,所以與node執(zhí)行過程有點(diǎn)類似。
pixelegos通過同步的require函數(shù)獲取tool、canvas和menu,后三者同樣通過require來執(zhí)行各自的依賴模塊,于是通過這樣一個(gè)遞歸的過程,pixelegos就執(zhí)行完畢了。
在Sea.js中,為了支持模塊的combo,存在一個(gè)js文件包含多個(gè)模塊的情況。根據(jù)依賴情況,使用grunt-cmd-concat可以將一個(gè)模塊以及其依賴的子模塊打包成一個(gè)js文件。
打包的方式有三種,self,relative和all。
seajs-modules
文件夾中的),transport,concat例如,我們seajs.use('/dist/pixelegos')
,解析為需要加載http://127.0.0.1:81/dist/pixelegos.js
這個(gè)文件,且這個(gè)文件是all全打包的。其加載過程如下:
use_
模塊,依賴于/dist/pixelegos
模塊,匿名的use_
模塊load
依賴,開始加載http://127.0.0.1:81/dist/pixelegos.js
模塊;http://127.0.0.1:81/dist/pixelegos.js
加載執(zhí)行,所有打包在里面的模塊被define
;http://127.0.0.1:81/dist/pixelegos.js
的onload
回調(diào)執(zhí)行,調(diào)用/dist/pixelegos
模塊的load,加載其依賴模塊,但依賴的模塊都加載好了;use_
加載完成,開始執(zhí)行期。針對(duì)每一次執(zhí)行期,對(duì)應(yīng)的加載依賴樹與整個(gè)模塊依賴樹是有區(qū)別的,因?yàn)樽幽K已經(jīng)加載好了的模塊,并不在加載樹中。
由于瀏覽器端與Node的環(huán)境差異,模塊存在加載期和執(zhí)行期,所以Sea.js中為模塊定義了六種狀態(tài)。
var STATUS = Module.STATUS = {
// 1 - The `module.uri` is being fetched
FETCHING: 1,
// 2 - The meta data has been saved to cachedMods
SAVED: 2,
// 3 - The `module.dependencies` are being loaded
LOADING: 3,
// 4 - The module are ready to execute
LOADED: 4,
// 5 - The module is being executed
EXECUTING: 5,
// 6 - The `module.exports` is available
EXECUTED: 6
}
分別為:
module.js是Sea.js的核心,我們來看一下,module.js是如何控制模塊加載過程的。
// 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()
}
模塊構(gòu)造完成,則調(diào)用mod.load()來同步其子模塊;直接跳過fetching這一步;mod.callback也是Sea.js不純粹的一點(diǎn),在模塊加載完成后,會(huì)調(diào)用這個(gè)callback。
// 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]()
}
}
}
模塊的狀態(tài)是最關(guān)鍵的,模塊狀態(tài)的流轉(zhuǎn)決定了加載的行為;
// 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是兩個(gè)非常關(guān)鍵的屬性,子模塊通過_waitings獲得父模塊,通過_remain來判斷模塊是否加載完成。
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
}
這預(yù)示著加載期結(jié)束,開始執(zhí)行期;
// 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
真的,整個(gè)Sea.js就是為了這行代碼能夠完美運(yùn)行
資源定位是Sea.js,或者說模塊加載器中非常關(guān)鍵部分。那什么是資源定位呢?
資源定位與模塊標(biāo)識(shí)相關(guān),而在Sea.js中有三種模塊標(biāo)識(shí)。
普通路徑與網(wǎng)頁(yè)中超鏈接一樣,相對(duì)于當(dāng)前頁(yè)面解析,在Sea.js中,普通路徑包有以下幾種:
// 假設(shè)當(dāng)前頁(yè)面是 http://example.com/path/to/page/index.html
// 絕對(duì)路徑是普通路徑:
require.resolve('http://cdn.com/js/a');
// => http://cdn.com/js/a.js
// 根路徑是普通路徑:
require.resolve('/js/b');
// => http://example.com/js/b.js
// use 中的相對(duì)路徑始終是普通路徑:
seajs.use('./c');
// => 加載的是 http://example.com/path/to/page/c.js
seajs.use('../d');
// => 加載的是 http://example.com/path/to/d.js
在define的factory中的相對(duì)路徑(..
.
)是相對(duì)標(biāo)識(shí),相對(duì)標(biāo)識(shí)相對(duì)當(dāng)前的URI來解析。
// File http://example.com/js/b.js
define(function(require) {
var a = require('./a');
a.doSomething();
});
// => 加載的是http://example.com/js/a.js
這與node模塊中相對(duì)路徑的解析一致。
不以.
或者'/'開頭的模塊標(biāo)識(shí)是頂級(jí)標(biāo)識(shí),相對(duì)于Sea.js的base路徑來解析。
// 假設(shè) base 路徑是:http://example.com/assets/
// 在模塊代碼里:
require.resolve('gallery/jquery/1.9.1/jquery');
// => http://example.com/assets/gallery/jquery/1.9.1/jquery.js
在node中即是在paths中搜索模塊(node_modules文件夾中)。
使用seajs.use啟動(dòng)模塊,如果不是頂級(jí)標(biāo)識(shí)或者是絕對(duì)路徑,就是相對(duì)于頁(yè)面定位;如果是頂級(jí)標(biāo)識(shí),就從Sea.js的模塊系統(tǒng)中加載(即base);如果是絕對(duì)路徑,直接加載;之后的模塊加載都是在define的factory中,如果是相對(duì)路徑,就是相對(duì)標(biāo)識(shí),相對(duì)當(dāng)前模塊路徑加載;如果是絕對(duì)路徑,直接加載;由此可見,在Sea.js中,模塊的配置被分割成2+x個(gè)地方:
由此可見,Sea.js確實(shí)海納百川。
1.在Sea.js中,使用data.cwd來代表當(dāng)前頁(yè)面的目錄,如果當(dāng)前頁(yè)面地址為http://www.dianping.com/promo/195800
,則cwd為http://www.dianping.com/promo/
;使用data.base來代表sea.js的加載地址,如果sea.js的路徑為http://i1.dpfile.com/lib/1.0.0/sea.js
,則base為http://i1.dpfile.com/lib/
。
“當(dāng) sea.js 的訪問路徑中含有版本號(hào)或其他東西時(shí),base 不會(huì)包含 seajs/x.y.z 字串。 當(dāng) sea.js 有多個(gè)版本時(shí),這樣會(huì)很方便”。看到這一句,我凌亂了,這Sea.js是多么的人性化!但是我覺得這似乎沒有必要。
2.seajs.use是,除了絕對(duì)路徑,其他都是相對(duì)于cwd定位,即如果模塊標(biāo)識(shí)為:
從需求上看,相對(duì)頁(yè)面地址定位在現(xiàn)實(shí)生活中并不太適用,如果頁(yè)面地址或者靜態(tài)文件的路徑稍微變化下,就跪了。
如果模塊標(biāo)識(shí)為絕對(duì)路徑:
3.除此之外,就是factory中的模塊標(biāo)識(shí)了:
在Sea.js的API中,define(factory)
,并沒有指明模塊的依賴項(xiàng),那Sea.js是如何獲得的呢?
這段是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
}
REQUIRE_RE
這個(gè)碩大無(wú)比的正則就是關(guān)鍵。推薦使用regexper來看看這個(gè)正則表達(dá)式。非native的函數(shù)factory我們可以通過的toString()方法獲取源碼,Sea.js就是使用REQUIRE_RE
在factory的源碼中匹配出該模塊的依賴項(xiàng)。
從
REQUIRE_RE
這么長(zhǎng)的正則來看,這里坑很多;在CommonJS的wrapper方案中可以使用JS語(yǔ)法分析器來獲取依賴會(huì)更準(zhǔn)確。
更多建議: