標籤:style blog http color java os io 檔案
之前面試時老問一個問題seajs 是怎麼載入js 檔案的
在網上找一些資料,覺得這個寫的不錯就轉載了,記錄一下,也學習一下
seajs 源碼解讀
seajs 簡單介紹
seajs是前端應用模組化開發的一種很好的解決方案。對於多人協作開發的、複雜龐大的前端項目尤其有用。簡單的介紹不多說,大家可以到seajs的官網seajs.org參看介紹。本文主要簡單地解讀一下seajs的源碼和模組化原理。如果有描述不實的地方,希望大家指正和交流。
註:本文的解析是基於seajs的2.2.1版本。
目錄結構
解壓seajs之後的src目錄結構如下:
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);
其他檔案的用途就不一一重複敘述了,看列表即可。
頁面如何動態載入js檔案
在解析seajs的源碼和原理之前,讓我們來回憶一下,在沒有seajs或者requirejs的情況下,最原始的動態指令碼載入方法是怎樣的。方法很簡單:其實就是建立一個script的標籤,設定了src為你想要載入的指令碼url,把script標籤append到Dom裡去就想了,so easy!沒錯,絕大部分模組載入js庫的原理都是如此。
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四個檔案。他們之間的依賴關係例如以下:
問題在於,如何找出模組裡的依賴關係,如何確保A在運行前已經載入了B等等。這些都是前端模組化和模組依賴需要解決的問題
模組化實現思路
seajs的模組化實現原理,說簡單其實不簡單,說複雜其實也不是很複雜。主要思路可以用下面這一段代碼來說明:
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])。只需要Regex來匹配require關鍵詞後面的參考關聯性即可。所以seajs中函數parseDependencies的寫法就像這樣(這一部分代碼在util-deps.js):
var SLASH_RE = /\\\\/gvar REQUIRE_RE = /"(?:\\"|[^"])*"|‘(?:\\‘|[^‘])*‘|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["‘])(.+?)\1\s*\)/gfunction 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://blog.segmentfault.com/civerzhu/1190000000471722