seajs是前端應用模塊化開發的一種很好的解決方案。對於多人協做開發的、複雜龐大的前端項目尤爲有用。簡單的介紹很少說,你們能夠到seajs的官網seajs.org參看介紹。本文主要簡單地解讀一下seajs的源碼和模塊化原理。若是有描述不實的地方,但願你們指正和交流。
注:本文的解析是基於seajs的2.2.1版本。javascript
解壓seajs以後的src目錄結構以下:html
intro.js -- 全局閉包頭部 sea.js -- 基本命名空間 util-lang.js -- 語言加強 util-events.js -- 簡易事件機制 util-path.js -- 路徑處理 util-request.js -- HTTP 請求 util-deps.js -- 依賴提取 module.js -- 核心代碼 config.js -- 配置 outro.js -- 全局閉包尾部
src目錄存放主要的seajs源代碼。各個文件的做用也如上面所示。其中,module.js是此次源碼解讀的核心,但我也會順帶介紹一下其餘文件的做用的。
sea.js對代碼比較簡單,其實就是聲明一下全局的seajs命名空間。
intro.js和outro.js則是咱們熟悉的匿名函數包裹基本代碼的方式,只是這裏比較特別的是,這段匿名函數被拆分紅intro.js和outro.js兩個文件。這樣的作法主要是方便調試,在調試的環境下,不引用intro.js和outro.js便可以直接在全局裏暴露seajs內部的接口,調試起來比較方便。intro.js和outro.js合併起來的代碼以下:前端
(function(global, undefined) { if (global.seajs) { return } // .... })(this);
其餘文件的用途就不一一重複敘述了,看列表便可。java
在解析seajs的源碼和原理以前,讓咱們來回憶一下,在沒有seajs或者requirejs的狀況下,最原始的動態腳本加載方法是怎樣的。方法很簡單:其實就是建立一個script的標籤,設置了src爲你想要加載的腳本url,把script標籤append到Dom裏去就想了,so easy!沒錯,絕大部分模塊加載js庫的原理都是如此。node
var script = document.createElement('script'); script.setAttribute('src', 'example.js'); script.onload = function() { console.log("script loaded!"); }; document.body.appendChild(script);
上述代碼便可以完成一次簡單的動態腳本加載。然而,seajs真正的核心在於處理模塊依賴的問題。在前端JS開發領域,尤爲是複雜的web應用,模塊依賴問題一直是使人頭疼的問題。
很簡單的道理,例如A、B、C、D四個模塊對應於A.js、B.js、C.js、D.js四個文件。他們之間的依賴關係例如如下:git
問題在於,如何找出模塊裏的依賴關係,如何確保A在運行前已經加載了B等等。這些都是前端模塊化和模塊依賴須要解決的問題。github
seajs的模塊化實現原理,說簡單其實不簡單,說複雜其實也不是很複雜。主要思路能夠用下面這一段代碼來講明:web
Module.define = function (id, deps, factory) { // 獲取代碼中聲明的依賴關係 deps = parseDependencies(factory.toString()); // 保存 Module.save(); // 匹配到url var url = Module.resolve(id); // 加載腳本 script.url = url; loadScript(); // 執行factory並保存模塊的引用 ... };
首先咱們來看看如何獲取代碼中聲明須要依賴的模塊。通常狀況下,seajs中同步加載模塊的寫法是相似這樣的:正則表達式
define('scripts/a', function(require, exports, module) { var factory = function() { var moduleB = require('scripts/b'); ... }; module.exports = factory; });
那麼須要獲取依賴的信息,咱們能夠藉助Function的toString方法,一個函數的toString方法是會返回函數自己的代碼的(對於JavaScript自身的函數,會返回[native code])。只須要正則表達式來匹配require關鍵詞後面的引用關係便可。因此seajs中函數parseDependencies的寫法就像這樣(這一部分代碼在util-deps.js):緩存
var SLASH_RE = /\\\\/g var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g function parseDependencies(code) { var ret = [] code.replace(SLASH_RE, "") // 匹配require關鍵詞,找出依賴關係 .replace(REQUIRE_RE, function(m, m1, m2) { if (m2) { ret.push(m2) } }) return ret }
而後找出代碼中聲明的依賴id,經過id來匹配正確的腳本url地址。這一部分的代碼在util-path.js
function id2Uri(id, refUri) { if (!id) return "" id = parseAlias(id) id = parsePaths(id) id = parseVars(id) id = normalize(id) var uri = addBase(id, refUri) uri = parseMap(uri) return uri }
這裏有個特別的地方,相似require('a/b/c')
這樣的寫法,seajs是如何知道腳本地址的絕對路徑的呢?道理很簡單,就是經過seajs本身往dom裏添加的id爲'seajsnode'的script節點或者是當前html中最後一個script節點,經過這些節點的src屬性獲取腳本的絕對路徑。
讓咱們把目光移回到核心的module.js中。seajs爲模塊的加載過程定義了6種狀態。
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
開始加載當前模塊
* SAVED
當前模塊加載完成並保存模塊數據
* LOADING
開始加載依賴的模塊
* LOADED
依賴模塊已經加載完成
* EXECUTING
當前模塊執行中
* EXECUTED
當前模塊執行完成
其實這一加載執行過程並不是線性的,當前模塊在加載所依賴的模塊的是,所依賴的模塊一樣也須要進行這一過程,直到全部的依賴都加載執行完畢,當前模塊纔開始執行。
在module.js中seajs中的一些方法說明了上述整個流程。
Module.use
構造一個沒有factory的模塊,開始整個加載流程,狀態初始化爲FETCHING到SAVED;Module.prototype.load
經過load方法,開始加載子模塊,狀態由SAVED到LOADING;Module.prototype.onload
當子模塊都加載完成後都會調用onload方法,狀態由LOADING到LOADED;Module.prototype.exec
加載過程都結束了,開始執行模塊,狀態由EXECUTING到EXECUTED;這裏每一個方法的詳細過程就不一一解析,有興趣的同窗能夠去看源碼。
實際上,seajs會對加載過的模塊保存一份引用在cachedMods中,在require的時候會先調用緩存中的模塊。
seajs.require = function(id) { var mod = Module.get(Module.resolve(id)) if (mod.status < STATUS.EXECUTING) { mod.onload() mod.exec() } return mod.exports } Module.get = function(uri, deps) { return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps)) }
前端模塊化一直是前端開發中比較重要的一點。前端開發相對其餘語言來講比較特殊,尤爲是對應大型Web項目的前端代碼,如何簡潔優雅地劃分模塊,如何管理這些模塊的依賴問題,這些都須要花必定的時間去認識和探討。所以,Common.js(致力於設計、規劃並標準化 JavaScript API)的誕生開啓了「 JavaScript 模塊化的時代」。前端領域的模塊化方案,像requireJS、SeaJS等都是Common.js的實踐者,對咱們規劃前端的代碼頗有幫助。然而,問題其實還有不少,seajs依然未能徹底知足前端模塊化開發,在性能問題、打包部署等方法還有着不足,不過技術的將來總在進步,相信之後會有更好的解決方法。
http://island205.github.io/HelloSea.js/
http://seajs.org/docs/#docs
http://chuansongme.com/account/wtp-notes