JS模塊載入器載入原理是怎麼樣的?
例如Require、Seajs模塊器載入的原理是怎麼樣的?模塊載入都有什麼方式?不同方式各有什麼利弊?
既然都打廣告,我也來補一個:zengjialuo/kittyjs · GitHub
-----------
我又來貢獻乾貨了。原理一:id即路徑 原則。
通常我們的入口是這樣的: require( [ "a", "b" ], callback ) 。這裡的 "a"、"b" 都是 ModuleId。通過 id 和路徑的對應原則,載入器才能知道需要載入的 js 的路徑。在這個例子里,就是 baseUrl + "a.js" 和 baseUrl + "b.js"。
但 id 和 path 的對應關係並不是永遠那麼簡單,比如在 AMD 規範里就可以通過配置 Paths 來給特定的 id 指配 path。
原理二:createElement("script") appendChild
知道路徑之後,就需要去請求。一般是通過 createElement("script") appendChild 去請求。這個大家都知道,不多說。有時候有的載入器也會通過 AJAX 去請求腳本內容。一般來說,需要給 &
簡單來說,就是通過 toString 這個方法得到 factory 的內容,然後用正則去匹配其中的 require( "moduleId" )。當然也可以不用正則。
這就是為什麼 require( var ); 這種帶變數的語句是不被推薦的,因為它會影響依賴分析。如果一定要用變數,可以用 require( [ var ] ) 這種非同步載入的方式。
原理五:遞歸載入在分析出模塊的依賴之後,我們需要遞歸去載入依賴模塊。用偽代碼來表達大概是這樣的:Module.prototype.load = function () {
var deps = this.getDeps();
for (var i = 0; i &< deps.length; i++) {
var m = deps[i];
if (m.state &< STATUS.LOADED) {
m.load();
}
}
this.state = STATUS.LOADED;
}
上面的代碼只是表達一個意思,實際上 load 方法很可能是非同步的,所以遞歸的返回要特殊處理下。
實現一個可用的載入器並沒有那麼簡單,比如你要處理循環依賴,還有各種各樣的牽一髮動全身的細節。但要說原理,大概就是這麼幾條。個人覺得,比起照著規範實現一個載入器,更加吸引人的是 AMD 或者 CommonJS 這些規範的完善和背後的設計思路。cmd 對於 nodejs 的使用者來說更加友好,使得類似 commonJS 模塊的寫法可以在瀏覽器中使用,同時解決了瀏覽器中模塊非同步載入的困擾。
關於 cmd 更詳細的內容可以移步specification/module.md at master · cmdjs/specification · GitHub我們可以通過實現一個簡單的 cmd loader,來學習其中的原理和細節。
模塊載入流程
下圖展示了一個 cmd loader 的模塊載入大體流程:
1. 首先,通過 use 方法來載入入口模塊,並接收一個回調函數, 當模塊載入完成, 會調用回調函數,並傳入對應的模塊。use 方法會 check 模塊有沒有緩存,如果有,則從緩存中獲取模塊,如果沒有,則創建並載入模塊。2. 獲取到模塊後,模塊可能還沒有 load 完成,所以需要在模塊上綁定一個 "complete" 事件,模塊載入完成會觸發這個事件,這時候才調用回調函數。3. 創建一個模塊時,id就是模塊的地址,通過創建 script 標籤的方式非同步載入模塊的代碼(factory),factory 載入完成後,會 check factory 中有沒有 require 別的子模塊: - 如果有,繼續載入其子模塊,並在子模塊上綁定 "complete" 事件,來觸發本身 的 "complete" 事件; - 如果沒有則直接觸發本身的 "complete" 事件。4. 如果子模塊中還有依賴,則會遞歸這個過程。5. 通過事件由里到外的傳遞,當所有依賴的模塊都 complete 的時候,最外層的入口模塊才會觸發 "complete" 事件,use 方法中的回調函數才會被調用。
功能劃分
理解了整個過程,那麼我們就來開始實現我們的代碼,我們暫且給這個載入器命名為 mcmd 吧。首先是載入器的功能模塊劃分:
- mcmd:入口文件,用於定義默認配置,參數,常量等,同時使用或載入其他的功能模塊;- define:實現 cmd 中的 "define" 方法;- require:實現 cmd 中的 "require" 方法;- use:實現 cmd 中的 "use" 方法;- module:模塊類,實現模塊的創建、載入、事件等功能;- load:用於獲取模塊,把模塊從新建和從 cache 中獲取封裝成統一的介面;
- promise:非同步任務處理器;- util:工具類函數;構建
我們使用 commonJS 的方式進行編碼,並使用 browserify 配合 gulp 來構建我們的項目。
var gulp = require("gulp");
var uglify = require("gulp-uglify");
var concat = require("gulp-concat");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var buffer = require("vinyl-buffer")
var pg = require("./package");
var versionName = pg.name + "." + pg.version
gulp.task("default", ["build"]);
gulp.task("build", function () {
browserify("./src/mcmd.js")
.bundle()
.pipe(source(versionName))
.pipe(buffer())
.pipe(concat(versionName + ".js"))
.pipe(gulp.dest("./prd"))
.pipe(uglify())
.pipe(concat(versionName + ".min.js"))
.pipe(gulp.dest("./prd"));
});
確定好了功能劃分和構建方式,下面我們就來實現每一個功能模塊:
入口文件 mcmd.js
將我們的 cmd loader 掛在 window.mcmd 上,把 define 方法也掛在 window.define 上,初始化其他的方法和配置。
var g = window;
g.define = require("./define");
g.mcmd = {
use: require("./use"),
require: require("./require"),
// 模塊緩存
modules: {},
// 默認配置
config: {
root: "/"
},
// 修改配置
setConfig: function (obj) {
for (var key in obj) {
this.config[key] = obj[key];
}
},
// 模塊狀態常量
MODULE_STATUS: {
PENDDING: 0,
LOADING: 1,
COMPLETED: 2,
ERROR: 3
}
};
use.js
實現了 mcmd.use 方法,接收兩個參數,第一個是id或者id數組,第二個是回調函數。內部會使用 load.js 來獲取模塊,並通過 promise 來處理獲取多個模塊的並發非同步場景。
var Promise = require("./promise");
var load = require("./load");
module.exports = function use(ids, callback) {
if (!Array.isArray(ids)) {
ids = [ids]
}
Promise.all(ids.map(function (id) {
return load(mcmd.config.root + id);
})).then(function (list) {
if (typeof callback === "function") {
callback.apply(window, list);
}
}, function (errorInfo) {
throw errorInfo;
});
}
load.js
獲取一個模塊,並綁定事件,接收兩個參數,一個是模塊id,一個是回調函數,並返回一個 promise 對象。當模塊 complete(載入完成)時,執行回調,同時 resolve 返回的 promise 對象。
var Promise = require("./promise");
var Module = require("./module");
var util = require("./util");
module.exports = function (id, callback) {
return new Promise(function (resolve, reject) {
var mod = mcmd.modules[id] || Module.create(id);
mod.on("complete", function () {
var exp = util.getModuleExports(mod);
if (typeof callback === "function") {
callback(exp);
}
resolve(exp);
});
mod.on("error", reject);
});
}
promise.js
詳見: 如何實現一個ECMAScript 6 的promise補丁 |
安·記
module.js
模塊的構造函數,實現了模塊的創建,載入,事件傳遞,狀態維護等。
// 構造函數
function Module(id) {
mcmd.modules[id] = this; // 緩存模塊
this.id = id;
this.status = mcmd.MODULE_STATUS.PENDDING; // 狀態
this.factory = null; // 執行代碼
this.dependences = null; //依賴
this.callbacks = {}; // 綁定的事件回調函數
this.load();
}
// 靜態方法創建模塊
Module.create = function (id) {
return new Module(id);
}
// 通過創建 script 標籤非同步載入模塊
Module.prototype.load = function () {
var id = this.id;
var script = document.createElement("script");
script.src = id;
script.onerror = function (event) {
this.setStatus(mcmd.MODULE_STATUS.ERROR, {
id: id,
error: (this.error = new Error("module can not load."))
});
}.bind(this);
document.head.appendChild(script);
this.setStatus(mcmd.MODULE_STATUS.LOADING);
}
// 事件綁定方法
Module.prototype.on = function (event, callback) {
(this.callbacks[event] || (this.callbacks[event] = [])).push(callback);
if (
(this.status === mcmd.MODULE_STATUS.LOADING event === "load") ||
(this.status === mcmd.MODULE_STATUS.COMPLETED event === "complete")
) {
callback(this);
}
if (this.status === mcmd.MODULE_STATUS.ERROR event === "error") {
callback(this, this.error);
}
}
// 事件觸發方法
Module.prototype.fire = function (event, arg) {
(this.callbacks[event] || []).forEach(function (callback) {
callback(arg || this);
}.bind(this));
}
// 設置狀態方法,並拋出相應的事件
Module.prototype.setStatus = function (status, info) {
if (this.status !== status) {
this.status = status;
switch (status) {
case mcmd.MODULE_STATUS.LOADING:
this.fire("load");
break;
case mcmd.MODULE_STATUS.COMPLETED:
this.fire("complete");
break;
case mcmd.MODULE_STATUS.ERROR:
this.fire("error", info);
break;
default:
break;
}
}
}
module.exports = Module;
define.js
實現 window.define 方法。接收一個參數 factory(cmd規範中不止一個,為了保持簡單,我們只實現一個),即模塊的代碼包裹函數。通過 getCurrentScript 這個函數獲取到當前執行腳本的 script 節點 src ,提取出模塊 id ,找到模塊對象。然後提取出 factory 中的依賴子模塊,如果沒有依賴,則直接觸發模塊的 "complete" 事件, 如果有依賴,則創建依賴的模塊,綁定事件並載入,等依賴的模塊載入完成後,再觸發 "complete" 事件。
var util = require("./util");
var Promise = require("./promise");
var Module = require("./module");
module.exports = function (factory) {
var id = getCurrentScript().replace(location.origin, "");
var mod = mcmd.modules[id];
var dependences = mod.dependences = getDenpendence(factory.toString());
mod.factory = factory;
if (dependences) {
Promise.all(dependences.map(function (id) {
return new Promise(function (resolve, reject) {
id = mcmd.config.root + id;
var depMode = mcmd.modules[id] || Module.create(id);
depMode.on("complete", resolve);
depMode.on("error", reject);
});
})).then(function () {
mod.setStatus(mcmd.MODULE_STATUS.COMPLETED);
}, function (error) {
mod.setStatus(mcmd.MODULE_STATUS.ERROR, error);
});
}
else {
mod.setStatus(mcmd.MODULE_STATUS.COMPLETED);
}
}
// 獲取當前執行的script節點
// 參考 getCurrentScript的改進
function getCurrentScript() {
var doc = document;
if(doc.currentScript) {
return doc.currentScript.src;
}
var stack;
try {
a.b.c();
} catch(e) {
stack = e.stack;
if(!stack window.opera){
stack = (String(e).match(/of linked script S+/g) || []).join(" ");
}
}
if(stack) {
stack = stack.split( /[@ ]/g).pop();
stack = stack[0] == "(" ? stack.slice(1,-1) : stack;
return stack.replace(/(:d+)?:d+$/i, "");
}
var nodes = head.getElementsByTagName("script");
for(var i = 0, node; node = nodes[i++];) {
if(node.readyState === "interactive") {
return node.className = node.src;
}
}
}
// 解析依賴,這裡只做簡單的提取,實際需要考慮更多情況,參考seajs
function getDenpendence(factory) {
var list = factory.match(/require(.+?)/g);
if (list) {
list = list.map(function (dep) {
return dep.replace(/(^require([""])|([""])$)/g, "");
});
}
return list;
}
require.js
var util = require("./util");
module.exports = function (id) {
id = mcmd.config.root + id;
var mod = mcmd.modules[id];
if (mod) {
return util.getModuleExports(mod);
}
else {
throw "can not get module by from:" + id;
}
}
module.exports.async = function (ids, callback) {
mcmd.use(ids, callback);
}
util.js
這裡只有一個 getModuleExports 方法, 接收一個模塊,返回模塊的介面。當模塊的 exports 屬性不存在時,說明模塊的 factory 沒有被執行過。這時我們需要執行下 factory,傳入 require, 創建的exports,以及 module 本身作為參數。最後獲取模塊的暴露的數據並返回。
module.exports = {
getModuleExports: function (mod) {
if (!mod.exports) {
mod.exports = {};
mod.factory(mcmd.require, mod.exports, mod);
}
return mod.exports;
}
};
這樣,整個 cmd loader 就基本完成了。這只是一個非常基礎的模塊載入器,主要是為了理解 cmd 的原理和實現方式,對於生產環境,推薦使用成熟的 seajs。
整個 mcmd 項目我都放在了 github 上,大家可以去看:https://github.com/hanan198501/mcmd。原文地址:如何實現一個 CMD 模塊載入器 |安·記
剛起床,醒醒腦,分別簡約的回答下三個問題:
Require、Seajs模塊器載入的原理是怎麼樣的?
1. 定義一套依賴規則, 如AMD CMD CommonJS Modules規範,規範即規則2. 載入入口文件及其依賴,原理即按依賴關係遞歸執行 document.createElement("script") 3. 維護模塊從初始到銷毀的生命周期模塊載入都有什麼方式?
1. 手動模式 - 人肉管理2. 自動模式- 模塊載入器管理
3. 混合模式 - 1,2結合不同方式各有什麼利弊?1. 首先千萬別拿著鎚子看啥都是釘子,依場景使用2. 基於約定的模塊依賴管理相比人肉是更好實踐,但謹記13. 使用了模塊管理器後,必然會引入複雜度,這是複雜度的轉移,如何駕馭這些也是成為優秀工程師的修鍊之路4. 使用模塊載入器後最頭疼的是構建發布問題,使用seajs的童鞋應該深有體會吧,(廣告時間)推薦 ModJS自動零配置構建seajs項目:mod/example/buildseajs at master · modjs/mod · GitHub零配置構建requrejs項目:mod/example/buildrequirejs at master · modjs/mod · GitHub就沒一個人貼點簡單易懂的代碼的嗎?我來好了。
function require(path) {
if (require.cache[path]) {
return require.cache[path].exports
}
var src = fs.readFileSync(path)
var code = new Function("exports, module", src)
var module = {exports:{}}
code(module.exports, module)
require.cache[path] = module
return module.exports
}
require.cache = Object.create(null)
本以為回答會沒人看呢,沒想到還有人點贊,那就解釋一下上面的代碼吧:
想要在JS中實現模塊系統,當然是像其它語言里一樣把一個模塊放入一個文件了,這個文件最終當然是被當做代碼運行的,但如何知道這個文件為外界提供了什麼呢?
方法有很多,一種方法是,讓這個文件里的代碼在一個函數內運行,然後返回想要提供給外界的介面。
另一種方法是,讓這個文件去修改由外部提供給它的一個對象以藉由這個對象來給外界提供介面。
而幾大模塊系統(Node的CommonJS,requirejs的AMD,seajs的CMD)都選擇了第二種方法,即讓模塊代碼去修改一個已存在的對象,不過seajs也支持通過模塊的代碼返回一個對象來導出模塊。
上面代碼的意義很明顯,把模塊對應的文件讀出來,通過文件的內容動態構建一個函數,構建出來的這個函數是有兩個參數的(上面幾個模塊系統的實現實際上把require函數也做為參數傳進去了),一個名為exports,一個名為module,而且exports是module對象的一個屬性,它本身也是個空對象。
然後將exports與module做為參數調用剛剛通過模塊文件的內容構建出來的函數,在模塊內部,實際上就是在修改exports對象或者是module.exports對象(如果你用過任意一個模塊系統這應該是很熟悉的)這個函數運行完成後,module這個對象應該就已經被修改為想要的樣子了,把得到的內容緩存以便下次直接返回,同時本次也返回得到的module就完事了。
至於文件的內容是同步獲取還是非同步獲取,那就得看具體環境和需求了,不是模塊載入器的原理,requires跟seajs都在瀏覽器端運行,當然只能選擇非同步載入了,但seajs的用法看起來地像是同步的,實現方式是載入完後不運行而是分析它的依賴然後遞歸載入它的依賴,直到所有的依賴都載入完成。
先上廣告,lithe.js是我11年寫的一個東東,現在業務線都在用的一個cmd格式載入器,參考seajs和requirejs的某一版本的幾個地方,有經過半年的優化和重寫(加了點自己的料)。
litheModule/lithe · GitHub
然後,我來回答問題,最近在寫一個開源的類medium編輯器,組織形式打算用模塊化開發,簡單么,而且本地調試也方便,雖然lithe已經足夠的小,只有2.7,500多行,但是覺得依然很重。。
so,寫了個簡單的只支持一種調用方式的amd格式的loader,通過必須定義標準路徑和id的方法,繞過了幾個代碼量較多的實現。題主可以來看看,沒壓縮只有80行。我相信你能看得懂……
iWo/iwo.loader.js at master · liulyliu/iWo · GitHub
原理我用大白話告訴你,通過一個入口,載入所有依賴,每次完成一次onload,查看是否還有未完成和未開始的腳本,一直到最後一個載入完畢(載入過程是不阻塞的,當然也還是會分級)之後,按照依賴關係依次執行,結果保存到cache,下次再跑,就不執行了,確保factory只執行一次。
大概就是這樣。。ps:這代碼有點漏洞和缺陷或者叫特性?看題主能不能發現了……建議先看普通載入器,LABjs Script Loader :: Project Description,再看文藝載入器,yepnope.js | A Conditional Loader For Your Polyfills!再看模塊載入器。
本質上模塊就是一個具有標識(uri)和對外介面的包機制,理解nodejs就理解了頁面的載入器。不同的是頁面有個load script的過程,偵聽onload回調。另外頁面還有combo邏輯,這些nodejs是沒有的。另外nodejs可以做到懶載入,頁面只能做到懶初始化,不過新的模塊規範更加突出強靜態化,懶載入應該不再會有。
先上廣告:
island205/HelloSea.js · GitHub,載入 CommonJS 到瀏覽器:island205/browserify-loader · GitHub。有時間再來填坑。模塊和載入是兩個部分
模塊是說你怎麼組織你的代碼 針對的是開發者載入是說代碼怎麼跑到瀏覽器去 針對的是機器 是運行時一開始這兩個部分是合在一起的 也就是所謂的seajs requirejs 優點是按需載入 具體沒實現過 不過就是類似於添加script標籤載入js文件 同時也要對依賴等問題進行管理
後來出現了一些工具如browserify gulp等打包的工具 真正實現了把這個過程分開 首先把代碼按照那些模塊系統(決定生態環境)打包生成為一個源碼包 然後直接通過瀏覽器原生支持的script link等直接引用就好 這方案的優勢在於可以在運行代碼前 把代碼優化 壓縮以及依賴等問題都給解決掉 更適合工程化
於是 兩種方案各有優劣 所以 我預測下一個解決方案是能把以上組合在一起使用的 那就是es2015 moduleyanhaijing/lodjs · GitHub 前段時間寫了個AMD載入器,只有500行代碼,如果研究源碼建議看這個吧,requirejs太複雜了
優點就不用說啦吧主要原理:其實可以簡單的想想成 非同步js載入 減輕網路請求 通過回調函數來進行分段載入 解析缺點就是: 不容易找bug
主流的「前端」JS載入器原理都是往DOM里動態插入一個
&
真正的裝載仍然是瀏覽器自身完成的;模塊依賴是載入器自己實現的,因為瀏覽器無法處理,但最終多模塊的裝載只是對以上步驟的重複(batch作為一種優化)而已。
「主流」的前端JS loader參考Essential JavaScript: the top five script loadersStealJS我個人用得比較多,背後原理相同,但可以在編譯時flatten整個依賴樹。我來裝逼啦,https://github.com/ErosZy/UME.js 兩年前寫的簡單的模塊載入。
前端時間自己專研js模塊化的知識,然後就看了sea.js的源碼。講一下sea.js裡面關於模塊載入的實現。(個人認為,要理解他的思路過程,最好的方法還是自己去將源碼看一遍。最先發布地址:sea.js源碼(Module.js核心代碼)) 在了解模塊載入過程之前,我們要先對源碼的一些變數,和sea.js Module模塊對象上的方法和屬性有一定的了解。 * 講解的風格是先列源碼,然後把源碼的執行過程表示出來,細節實現請讀者閱讀講解上方的源碼。如果你不想看源碼,可以直接跳到源碼下方的講解部分。sea.js整體架構
sea.js3.0.0版本,源碼整體代碼(算上注釋,回車符)有1000來行。從github上clone下來,我們可以發現src下面有這麼幾個文件。
intro.js // 源碼頂部
outro.js // 源碼底部
util-cs.js // (內部工具)獲取當前載入中的script標籤隊形
util-deps.js // (內部工具)解析某一模塊的依賴模塊
util-events.js // (內部工具)sea.js的事件系統
util-lang.js // (內部工具)sea.js的類型判斷
util-path.js // (內部工具)sea.js的路徑解析
util-request.js // (內部工具)請求載入文件
standlone.js // (縮影代碼)模塊實現的簡易版本
module.js // (核心代碼)模塊的實現
config.js // (核心代碼)模塊配置
```
每一個文件的代碼都有它的作用,讓我們繼續看下去
以下內容第一遍可以快速瀏覽,然後在下面過程講解中回頭查閱相關變數屬性的作用。
sea.js源碼 - 變數說明var cachedMods = seajs.cache = {}
var anonymousMeta
var fetchingList = {}
var fetchedList = {}
var callbackList = {}
作用說明:
- cachedMods :緩存隊列,存儲已經載入好的模塊
- anonymousMeta : 無factory的模塊
- fetchingList : 等待載入模塊隊列
- fetchedlist : 載入中的模塊隊列
- callbackList : 模塊回調執行隊列
sea.js源碼 - 模塊的狀態
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,
// 7 - 404
ERROR: 7
}
分別為:
- FETCHING:開始從服務端載入模塊
- SAVED:模塊載入完成
- LOADING:載入依賴模塊中
- LOADED:依賴模塊載入完成
- EXECUTING:模塊執行中
- EXECUTED:模塊執行完成
- ERROR:模塊載入出現錯誤
function Module(uri, deps) {
this.uri = uri
this.dependencies = deps || []
this.deps = {} // Ref the dependence modules
this.status = 0
this._entry = []
}
sea.js源碼 - 模塊Module上的函數方法模塊的構造函數:需要傳入兩個參數,模塊的文件路徑和依賴數組。
每個模塊包含以下屬性:
- uri : 模塊實例的標識,通常為一個模塊的絕對路徑
- dependencies : 模塊實例依賴的模塊uri數組,裡面放置的的是每個依賴模塊的uri
- dps : 模塊實例依賴的模塊對象數組,裡面放置的是每個依賴模塊對象Module實例。
- status : 模塊的狀態,作為下一步如何處理模塊的依據
- _entry : 所依賴的模塊載入完之後需要執行的 模塊
// Fetch a module
// 載入一個模塊
Module.prototype.fetch = function(requestCache){
}
// Resolve id to uri
// 根據模塊的 id 轉化為 uri
Module.resolve = function(id, refUri) {
}
// Define a module
// 定義一個模塊
Module.define = function (id, deps, factory) {
}
// Save meta data to cachedMods
// 將 Module 數據存儲到 cachedMods 緩存中
Module.save = function(uri, meta) {
}
// Get an existed module or create a new one
// 獲取一個已存在的模塊 或 創建一個新的模塊
Module.get = function(uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}
// Use function is equal to load a anonymous module
// 載入一個根節點模塊
Module.use = function (ids, callback, uri) {
}
// Resolve module.dependencies
// 解析模塊的 dependencies 屬性,解析轉換為uri
Module.prototype.resolve = function() {
}
// Load module.dependencies and fire onload when all done
// 載入 模塊依賴 ,並執行onload當所有的依賴模塊載入完
Module.prototype.load = function() {
}
// Call this method when module is loaded
// 當模塊載入好之後執行的方法
Module.prototype.onload = function() {
}
// Call this method when module is 404
// 當模塊 404 的時候執行該方法
Module.prototype.error = function() {
}
// Execute a module
// 執行模塊代碼
Module.prototype.exec = function () {
}
// Fetch a module
// 載入一個模塊資源
Module.prototype.fetch = function(requestCache) {
}
&
&
&
&
&
&
&
&
&
&
&
&
&
define(function(require, exports, module) {
var a1 = require("./a1");
console.log(a1.msg);
});
&
define(function(require, exports, module) {
var msg = "a1";
exports.msg = msg;
});
代碼執行流程:
module.js如何控制模塊載入過程- sea.js源碼 - 模塊的啟動從 use 方法開始
// Get an existed module or create a new one
Module.get = function(uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}
// 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._entry.push(mod)
mod.history = {}
mod.remain = 1
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 delete mod.history delete mod.remain delete mod._entry } mod.load() }
- Module.get() 方法是獲取一個存在的模塊,如果該模塊不存在,創建並返回一個新的模塊。
- 如果不記得 Module構造函數上屬性和方法,返回上面查閱。
- Module.use() 這裡做了模塊的初始化。
mod._entry.push(mod)
// 先是將自身模塊放置到_entry,為了傳遞給後面依賴模塊,後面會講。
mod.remian = 1// 表示還有多個依賴沒有載入,這裡表示的啟動文件。use引入的文件。
mod.callback = function() {
……
var uris = mod.resolve()
// 解析模塊依賴的 uri 數組
……
exports[i] = cachedMods[uris[i]].exec()
// 執行依賴介面並將介面存儲在一個數組中
//
……
// 將依賴模塊暴露的介面傳遞過來。}
// 這裡面是回調進行處理
然後執行 mod.load()
- sea.js源碼 - 初始完Module ,執行Module.load()
// Load module.dependencies and fire onload when all done
// 載入 模塊依賴 ,並執行onload當所有的依賴模塊載入完
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)
for (var i = 0, len = uris.length; i &< len; i++) {
mod.deps[mod.dependencies[i]] = Module.get(uris[i])
}
// Pass entry to it"s dependencies
mod.pass()
// If module has entries not be passed, call onload
if (mod._entry.length) {
mod.onload()
return
}
// Begin parallel loading
var requestCache = {}
var m
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]()
}
}
}
// 將最終需要執行的模塊 一層一層 的傳遞下去,直至沒有依賴
Module.prototype.pass = function() {
var mod = this
var len = mod.dependencies.length
for (var i = 0; i &< mod._entry.length; i++) {
var entry = mod._entry[i]
var count = 0
for (var j = 0; j &< len; j++) {
var m = mod.deps[mod.dependencies[j]]
// If the module is unload and unused in the entry, pass entry to it
if (m.status &< STATUS.LOADED !entry.history.hasOwnProperty(m.uri)) {
entry.history[m.uri] = true
count++
m._entry.push(entry)
if(m.status === STATUS.LOADING) {
m.pass()
}
}
}
// If has passed the entry to it"s dependencies, modify the entry"s count and del it in the module
if (count &> 0) {
entry.remain += count - 1
mod._entry.shift()
i--
}
}
}
- Module.load() : 載入 模塊依賴 ,並執行onload當所有的依賴模塊載入完
Module.prototype.load = function() {
……
var uris = mod.resolve();
// 解析模塊依賴的 uri 數組
mod.deps[mod.dependencies[i]] = Module.get(uris[i])
// 給模塊的依賴Module對象數組 初始化,並用uri作為標誌
……
pass();
/*
pass函數代碼:
Module.prototype.pass = function() {
……
var len = mod.dependencies.length
for (var i = 0; i &< mod._entry.length; i++) {
var entry = mod._entry[i];
……
for (var j = 0; j &< len; j++) {
// 判斷是否未載入,並且未傳遞過_entry
if (m.status &< STATUS.LOADED !entry.history.hasOwnProperty(m.uri)) {
……
m._entry.push(entry)
//mod._entry中的模塊會被傳遞給mod依賴的模塊的_entry,
……
}
}
if(count&>0){
entry.remain += count - 1
mod._entry.shift()
i--
//count 表示的就是還需要載入的模塊
//將mod._entry中的每個模塊的remain屬性加上相應的依賴模塊個數,然後將自己從_entry中移除。
// i-- ,使i減一,再次遍歷,直至count &<= 0
}
……
}
}
*/
// If module has entries not be passed, call onload
// 如果該模塊已經沒有依賴了
if (mod._entry.length) {
mod.onload()
return
}
// 並行載入文件
var requestCache = {}
// 緩存 需要載入的文件清單
for (i = 0; i &< len; i++) {
m = cachedMods[uris[i]]
if (m.status &< STATUS.FETCHING) {
// 如果模塊還未從伺服器載入過來,執行fetch().
// fetch() 將每個模塊m從伺服器獲取對應的js載入到頁面,並綁定到requestCache對象上
m.fetch(requestCache)
}
else if (m.status === STATUS.SAVED) {
// 如果模塊已經載入成功,執行load()
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]()
// 遍歷requestCache對象,並根據 URI 執行seajs.request
// seajs.request() 源碼在util-request中,詳細講解在下方。
}
}
}
補充,pass()函數的目的就是 將_entry的模塊傳遞給依賴的模塊,通過依賴的模塊再傳遞給依賴的模塊所依賴的模塊,一層層傳遞一下去,直到已經沒有依賴的模塊的那一層,這時候會調用mod.onload(),再根據remain判斷是否所有的依賴已經載入完畢決定是否執行該特殊模塊。
- sea.js源碼 - 模塊還未從服務載入: Module.fetch()
// Fetch a module
// 載入一個模塊資源
Module.prototype.fetch = function(requestCache) {
var mod = this
var uri = mod.uri
mod.status = STATUS.FETCHING
// Emit `fetch` event for plugins such as combo plugin
var emitData = { uri: uri }
emit("fetch", emitData)
var requestUri = emitData.requestUri || uri
// Empty uri or a non-CMD module
if (!requestUri || fetchedList.hasOwnProperty(requestUri)) {
mod.load()
return
}
if (fetchingList.hasOwnProperty(requestUri)) {
callbackList[requestUri].push(mod)
return
}
fetchingList[requestUri] = true
callbackList[requestUri] = [mod]
// Emit `request` event for plugins such as text plugin
emit("request", emitData = {
uri: uri,
requestUri: requestUri,
onRequest: onRequest,
charset: isFunction(data.charset) ? data.charset(requestUri) : data.charset,
crossorigin: isFunction(data.crossorigin) ? data.crossorigin(requestUri) : data.crossorigin
})
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset, emitData.crossorigin)
}
function onRequest(error) {
delete fetchingList[requestUri]
fetchedList[requestUri] = true
// Save meta data of anonymous module
if (anonymousMeta) {
Module.save(uri, anonymousMeta)
anonymousMeta = null
}
// Call callbacks
var m, mods = callbackList[requestUri]
delete callbackList[requestUri]
while ((m = mods.shift())) {
// When 404 occurs, the params error will be true
if(error === true) {
m.error()
}
else {
m.load()
}
}
}
}
在fetch(),
(一)為 當前模塊在 fetchingList中設置標識,存儲到 callbackList 數組中。(二)將sendRequest 函數添加到requestCache對象,用於調用載入模塊。【sendRequest() 方法是從伺服器請求資源到頁面中,調用seajs.request()。那麼這個seajs.request如何將模塊文件請求到頁面呢?具體詳情請查看:sea.js源碼(seajs.request()請求資源)】
- 過程重點介紹
……
fetchingList[requestUri] = true
callbackList[requestUri] = [mod]
……
emit("request", emitData = {
uri: uri,
requestUri: requestUri,
onRequest: onRequest,
charset: isFunction(data.charset) ? data.charset(requestUri) : data.charset,
crossorigin: isFunction(data.crossorigin) ? data.crossorigin(requestUri) : data.crossorigin
})
/*
* uri : 資源uri,一般為相對路徑
* requestUri : 模塊資源路徑
* onRequest : 請求到資源的時候的回調
* charset : 文件編碼
* crossorigin : 設置跨域屬性,配置元素獲取數據的CORS請求
*/if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
// 給 requestCache 對象添加屬性sendRequest函數 ,以requestUri為標識,
}function sendRequest() {}
function onRequest(error) {}
- sea.js源碼 - 當模塊已經沒有依賴了, 執行onload()
// If module has entries not be passed, call onload
// 如果該模塊已經沒有依賴了
if (mod._entry.length) {
mod.onload()
return
}
- 上一節我們講 pass() 將_entry的模塊傳遞給依賴的模塊,這樣一層一層傳遞下去,當傳到沒有依賴的模塊的時候,mod._entry.length 不為0假值。接下來中 mod.onload
// Call this method when module is loaded
// 當模塊載入好之後執行的方法
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
// When sometimes cached in IE, exec will occur before onload, make sure len is an number
for (var i = 0, len = (mod._entry || []).length; i &< len; i++) {
var entry = mod._entry[i]
if (--entry.remain === 0) {
entry.callback()
}
}
delete mod._entry
}
這段代碼很好理解,將模塊的狀態轉為 LOADED ,判斷一下entry.remain是否為0,為0說明所依賴的所有模塊已經載入完畢, 然後執行我們從最頂層的模塊 一層一層傳遞過來的 _entry,即頂層特殊模塊。
if (--entry.remain === 0) {
entry.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 delete mod.history delete mod.remain delete mod._entry }
我們可以看到
for (var i = 0, len = uris.length; i &< len; i++) {
exports[i] = cachedMods[uris[i]].exec()
// 需要執行前面我們所緩存載入過來的模塊,賦值給一個數組以存儲各模板介面。
}
if (callback) {
callback.apply(global, exports)
// 將介面數組作為參數傳遞,並將我們定義的函數放到全局作用域執行
}
理所應該,我們接下來就應該解讀 exec() 這個執行模塊的方法。但是我們必須了解 Module.define(ids,deps,factory(require,exports,module)) 這個在js文件中定義模塊的方法。
這裡Module.define(),Module.exec()我獨立開來,有興趣的小夥伴請移步seajs源碼(define,exec定義和執行模塊代碼)
基本原理是動態生成script標籤,比如requirejs,seajs。還有一些是ajax請求js代碼,然後eval執行的。另外可以關注一下MT.手機騰訊網,基於localstorage來做到路字元級別的增量更新
可以買本司徒正美的&<&
簡單來說就是從入口 js 往裡檢查依賴,通過 document.createElement("script") 動態生成 script 標籤。要學習弄懂某個東西的原理,最好的方式就是自己寫一個。比如我就沒看 requirejs/ seajs 源碼的情況下憑著自己的想像寫了個模塊載入器:yetone/snakejs · GitHub
我寫過個簡單的amd,沒有做靜態打包,都是每次請求時現拼的
稍微會點後端都可以寫出來一個比較簡單的amd,甚至一個打包發布工具假設a引了b和c,c引了d依賴的關鍵字是_import開始:讀入a.js:_import b;
_import c;a的內容
替換_import,第一次遞歸後變成:
b的內容;
import d;c的內容;a的內容
然後把上述內容第二次遞歸,變成:
b的內容;
d的內容;c的內容;a的內容
然後檢測沒有更多_import了,於是將以上內容輸出到最終文件。
cmd我還沒遇到適合的場景,所以也沒搞過...https://github.com/frankLife/practice/blob/master/tryLoader/doc/%E6%96%B0%E6%96%87%E6%A1%A3.md
https://github.com/frankLife/practice/blob/master/tryLoader/doc/%E6%96%B0%E6%96%87%E6%A1%A32.md關於sea的簡單分析。。可以了解下。。其實就是載入script標籤
推薦閱讀:
※HTML 標籤屬性的全稱?
※如何評價性能大幅提升的Chrome 53?
※安卓工程師轉做前端,有什麼好的框架推薦?
※Markdown編輯器 做成 WYSIWYG(所見即所得)形式會不會有什麼弊端?
※如何看待 React 的替代框架 Preact?
TAG:前端開發 | JavaScript | SeaJS | RequireJS |