seajs 源碼解讀

seajs 簡單介紹

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

頁面如何動態加載js文件

在解析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
  • B 依賴 C和D

問題在於,如何找出模塊裏的依賴關係,如何確保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來匹配腳本的url地址

而後找出代碼中聲明的依賴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

相關文章
相關標籤/搜索