Sea.js 是如何工作的?

2020-04-26 10:01 更新

Sea.js是如何工作的?

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

寫了這么多,必須證明一下本書并不是一份乏味的使用文檔,我們來深入看看Sea.js,搞清楚它時(shí)如何工作的吧!

CMD規(guī)范

要想了解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è)額外的東西:

  1. 模塊的管理
  2. 模塊從服務(wù)端的同步

即Sea.js必須分為模塊加載期和執(zhí)行期。加載期需要將執(zhí)行期所有用到的模塊從服務(wù)端同步過來,在再執(zhí)行期按照代碼的邏輯順序解析執(zhí)行模塊。本身執(zhí)行期與node的運(yùn)行期沒什么區(qū)別。

所以Sea.js需要三個(gè)接口:

  1. define用來wrapper模塊,指明依賴,同步依賴;
  2. use用來啟動(dòng)加載期;
  3. require關(guān)鍵字,實(shí)際上是執(zhí)行期的橋梁。

并不太喜歡Sea.js的use API,因?yàn)槠浠卣{(diào)函數(shù)并沒有使用與Define一樣的參數(shù)列表。

模塊標(biāo)識(shí)(id)

模塊id的標(biāo)準(zhǔn)參考Module Identifiers,簡(jiǎn)單說來就是作為一個(gè)模塊的唯一標(biāo)識(shí)。

出于學(xué)習(xí)的目的,我將它們翻譯引用在這里:

  1. 模塊標(biāo)識(shí)由數(shù)個(gè)被斜杠(/)隔開的詞項(xiàng)組成;
  2. 每次詞項(xiàng)必須是小寫的標(biāo)識(shí)、“.”或“..”;
  3. 模塊標(biāo)識(shí)并不是必須有像“.js”這樣的文件擴(kuò)展名;
  4. 模塊標(biāo)識(shí)不是相對(duì)的,就是頂級(jí)的。相對(duì)的模塊標(biāo)識(shí)開頭要么是“.”,要么是“..”;
  5. 頂級(jí)標(biāo)識(shí)根據(jù)模塊系統(tǒng)的基礎(chǔ)路徑來解析;
  6. 相對(duì)的模塊標(biāo)識(shí)被解釋為相對(duì)于某模塊的標(biāo)識(shí),“require”語(yǔ)句是寫在這個(gè)模塊中,并在這個(gè)模塊中調(diào)用的。

模塊(factory)

顧名思義,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。

依賴(dependencies)

依賴就是一個(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模塊間的依賴的呢?

node的方式-同步的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è)模塊,然后通過注入的exportsmodule這兩個(gè)關(guān)鍵字獲取該模塊的接口,將接口暴露出來給usegreet.js使用,即通過greet這個(gè)對(duì)象來引用這些接口。例如,helloJavaScript這個(gè)函數(shù)。詳細(xì)細(xì)節(jié)可以參看node源碼中的module.js。

node的模塊方案的特點(diǎn)如下:

  1. 使用require、exports和module作為模塊化組織的關(guān)鍵字;
  2. 每個(gè)模塊只加載一次,作為單例存在于內(nèi)存中,每次require時(shí)使用的是它的接口;
  3. require是同步的,通俗地講,就是node運(yùn)行A模塊,發(fā)現(xiàn)需要B模塊,會(huì)停止運(yùn)行A模塊,把B模塊加載好,獲取的B的接口,才繼續(xù)運(yùn)行A模塊。如果B模塊已經(jīng)加載到內(nèi)存中了,當(dāng)然require B可以直接使用B的接口,否則會(huì)通過fs模塊化同步地將B文件內(nèi)存,開啟新的上下文解析B模塊,獲取B的API。

實(shí)際上node如果通過fs異步的讀取文件的話,require也可以是異步的,所以曾經(jīng)node中有require.async這個(gè)API。

Sea.js的方式-加載期與執(zhí)行期

由于在瀏覽器端,采用與node同樣的依賴加載方式是不可行的,因?yàn)橐蕾囍挥性趫?zhí)行期才能知道,但是此時(shí)在瀏覽器端,我們無(wú)法像node一樣直接同步地讀取一個(gè)依賴文件并執(zhí)行!我們只能采用異步的方式。于是Sea.js的做法是,分成兩個(gè)時(shí)期——加載期和執(zhí)行期;

的確,我們可以使用同步的XHR從服務(wù)端加載依賴,但是本身就是單進(jìn)程的JavaScript還需要等待文件的加載,那性能將大打折扣。

  • 加載期:即在執(zhí)行一個(gè)模塊之前,將其直接或間接依賴的模塊從服務(wù)器端同步到瀏覽器端;
  • 執(zhí)行期:在確認(rèn)該模塊直接或間接依賴的模塊都加載完畢之后,執(zhí)行該模塊。

加載期

不難想見,模塊間的依賴就像一棵樹。啟動(dòng)模塊作為根節(jié)點(diǎn),依賴模塊作為葉子節(jié)點(diǎn)。下面是pixelegos的依賴樹:

loadingperiod

如上圖,在頁(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í)行期,執(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。

  • self,只是自己做了transport
  • relative,將多有相對(duì)路徑的模塊transport,concat
  • all,包括相對(duì)路徑模塊和庫(kù)模塊(即在seajs-modules文件夾中的),transport,concat

例如,我們seajs.use('/dist/pixelegos'),解析為需要加載http://127.0.0.1:81/dist/pixelegos.js這個(gè)文件,且這個(gè)文件是all全打包的。其加載過程如下:

加載方式

  1. 在use時(shí),定義一個(gè)匿名的use_模塊,依賴于/dist/pixelegos模塊,匿名的use_模塊load依賴,開始加載http://127.0.0.1:81/dist/pixelegos.js模塊;
  2. http://127.0.0.1:81/dist/pixelegos.js加載執(zhí)行,所有打包在里面的模塊被define;
  3. http://127.0.0.1:81/dist/pixelegos.jsonload回調(diào)執(zhí)行,調(diào)用/dist/pixelegos模塊的load,加載其依賴模塊,但依賴的模塊都加載好了;
  4. 通知匿名的use_加載完成,開始執(zhí)行期。

針對(duì)每一次執(zhí)行期,對(duì)應(yīng)的加載依賴樹與整個(gè)模塊依賴樹是有區(qū)別的,因?yàn)樽幽K已經(jīng)加載好了的模塊,并不在加載樹中。

Sea.js的實(shí)現(xiàn)

模塊的狀態(tài)

由于瀏覽器端與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
}

分別為:

  • FETCHING:開始從服務(wù)端加載模塊
  • SAVED:模塊加載完成
  • LOADING:加載依賴模塊中
  • LOADED:依賴模塊加載完成
  • EXECUTING:模塊執(zhí)行中
  • EXECUTED:模塊執(zhí)行完成

module.js是Sea.js的核心,我們來看一下,module.js是如何控制模塊加載過程的。

如何確定整個(gè)依賴樹加載好了呢?

  1. 定義A模塊,如果有模塊依賴于A,把該模塊加入到等待A的模塊隊(duì)列中;
  2. 加載A模塊,狀態(tài)變?yōu)镕ETCHING
  3. A加載完成,獲取A模塊依賴的BCDEFG模塊,發(fā)現(xiàn)B模塊沒有定義,而C加載中,D自己已加載好,E加載子模塊中,F(xiàn)加載完成,運(yùn)行中,G已經(jīng)解析好,SAVED;
  4. 由于FG本身以及子模塊都已加載好,因此A模塊要確定已經(jīng)加載好了,必須等待BCDE加載好;開始加載必須的子模塊,LOADING;
  5. 針對(duì)B重復(fù)步驟1;
  6. 將A加入到CDE的等待隊(duì)列中;
  7. BCDE加載好之后都會(huì)從自己的等待隊(duì)列中取出等待自己加載好的模塊,通知A自己已經(jīng)加載好了;
  8. A每次收到子模塊加載好的通知,都看一遍自己依賴的模塊是否狀態(tài)都變成了加載完成,如果加載完成,則A加載完成,A通知其等待隊(duì)列中的模塊自己已加載完成,LOADED;

加載過程

  • Sea.use調(diào)用Module.use構(gòu)造一個(gè)沒有factory的模塊,該模塊即為這個(gè)運(yùn)行期的根節(jié)點(diǎn)。
// 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方法中,獲取子模塊,加載子模塊,在子模塊加載完成后,會(huì)觸發(fā)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]()
        }
    }
}

模塊的狀態(tài)是最關(guān)鍵的,模塊狀態(tài)的流轉(zhuǎn)決定了加載的行為;

  • 是否觸發(fā)onload是由模塊的_remian屬性來確定,在load和子模塊的onload函數(shù)中都對(duì)_remain進(jìn)行了計(jì)算,如果為0,則表示模塊加載完成,調(diào)用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是兩個(gè)非常關(guān)鍵的屬性,子模塊通過_waitings獲得父模塊,通過_remain來判斷模塊是否加載完成。

  • 當(dāng)這個(gè)沒有factory的根模塊觸發(fā)onload之后,會(huì)調(diào)用其方法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
}

這預(yù)示著加載期結(jié)束,開始執(zhí)行期;

  • 而執(zhí)行期相對(duì)比較無(wú)腦,首先是直接調(diào)用根模塊依賴模塊的exec方法獲取其exports,用它們來調(diào)用use傳經(jīng)來的callback。而子模塊在執(zhí)行時(shí),都是按照標(biāo)準(zhǔn)的模塊解析方式執(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

相對(duì)標(biāo)識(shí)

在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ì)路徑的解析一致。

頂級(jí)標(biāo)識(shí)

不以.或者'/'開頭的模塊標(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è)地方:

  • 與頁(yè)面放在一起;
  • 與Sea.js放在一起;
  • 通過絕對(duì)路徑添加更多的模塊源。

由此可見,Sea.js確實(shí)海納百川。

獲取真實(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í)為:

  • './a',則真實(shí)加載路徑為http://www.dianping.com/promo/a.js;
  • '/a',則為http://www.dianping.com/a.js;
  • '../a',則為http://www.dianping.com/a.js;

從需求上看,相對(duì)頁(yè)面地址定位在現(xiàn)實(shí)生活中并不太適用,如果頁(yè)面地址或者靜態(tài)文件的路徑稍微變化下,就跪了。

如果模塊標(biāo)識(shí)為絕對(duì)路徑:

3.除此之外,就是factory中的模塊標(biāo)識(shí)了:

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)