其實本文的標題應該是「爲何我不推薦使用 AMD 的 Simplified CommonJS wrapping」,但太長了很差看,爲了美觀我只能砍掉一截。 node
它是什麼?
爲了複用已有的 CommonJS 模塊,AMD 規定了 Simplified CommonJS wrapping,而後 RequireJS 實現了它(前後順序不必定對)。它提供了相似於 CommonJS 的模塊定義方式,以下:python
define(
function
(require, exports, module) {
var
A = require(
'a'
);
return
function
() {};
});
|
這樣,模塊的依賴能夠像 CommonJS 同樣「就近定義」。但就是這個看上去一箭雙鵰的作法,給你們帶來了不少困擾。git
它作了什麼?
因爲 RequireJS 是最流行的 AMD 加載器,後續討論都基於 RequireJS 進行。github
直接看 RequireJS 這部分邏輯:
//If no name, and callback is a function, then figure out if it a
//CommonJS thing with dependencies.
if
(!deps && isFunction(callback)) {
deps = [];
if
(callback.length) {
callback
.toString()
.replace(commentRegExp,
''
)
.replace(cjsRequireRegExp,
function
(match, dep) {
deps.push(dep);
});
deps = (callback.length === 1 ? [
'require'
] : [
'require'
,
'exports'
,
'module'
]).concat(deps);
}
}
|
能夠看到,爲了支持 CommonJS Wrapper 這種寫法,define 函數裏須要作這些事情:
- 經過
factory.toString()
拿到 factory 的源碼; - 去掉源碼中的註釋(避免匹配到註釋掉的依賴模塊);
- 經過正則匹配
require
的方式獲得依賴信息;
寫模塊時要把 require
當成保留字。模塊加載器和構建工具都要實現上述邏輯。
對於 RequireJS,本文最開始定義的模塊,最終會變成:
define([
'a'
],
function
(require, exports, module) {
var
A = require(
'a'
);
return
function
() {};
});
|
等價於:
define([
'a'
],
function
(A) {
return
function
() {};
});
|
結論是,CommonJS Wrapper 只是書寫上兼容了 CommonJS 的寫法,模塊運行邏輯並不會改變。
AMD 運行策略
AMD 運行時核心思想是「Early Executing」,也就是提早執行依賴。這個好理解:
//main.js
define([
'a'
,
'b'
],
function
(A, B) {
//運行至此,a.js 和 b.js 已下載完成(運行於瀏覽器的 Loader 必須如此);
//A、B 兩個模塊已經執行完,直接可用(這是 AMD 的特性);
return
function
() {};
});
|
我的以爲,AMD 的這個特性有好有壞:
首先,儘早執行依賴能夠儘早發現錯誤。上面的代碼中,假如 a 模塊中拋異常,那麼 main.js 在調用 factory 方法以前必定會收到錯誤,factory 不會執行;若是按需執行依賴,結果是:1)沒有進入使用 a 模塊的分支時,不會發生錯誤;2)出錯時,main.js 的 factory 方法極可能執行了一半。
另外,儘早執行依賴一般能夠帶來更好的用戶體驗,也容易產生浪費。例如模塊 a 依賴了另一個須要異步加載數據的模塊 b,儘早執行 b 可讓等待時間更短,同時若是 b 最後沒被用到,帶寬和內存開銷就浪費了;這種場景下,按需執行依賴能夠避免浪費,可是帶來更長的等待時間。
我我的更傾向於 AMD 這種作法。舉一個不太恰當的例子:Chrome 和 Firefox 爲了更好的體驗,對於某些類型的文件,點擊下載地址後會詢問是否保存,這時候實際上已經開始了下載。有時候等了好久才點確認,會開心地發現文件已經下好;若是點取消,瀏覽器會取消下載,已下載的部分就浪費了。
瞭解到 AMD 這個特性後,再來看一段代碼:
//mod1.js
define(
function
() {
console.log(
'require module: mod1'
);
return
{
hello:
function
() {
console.log(
"hello mod1"
);
}
};
});
|
//mod2.js
define(
function
() {
console.log(
'require module: mod2'
);
return
{
hello:
function
() {
console.log(
"hello mod2"
);
}
};
});
|
//main.js
define([
'mod1'
,
'mod2'
],
function
(mod1, mod2) {
//運行至此,mod1.js 和 mod2.js 已經下載完成;
//mod一、mod2 兩個模塊已經執行完,直接可用;
console.log(
'require module: main'
);
mod1.hello();
mod2.hello();
return
{
hello:
function
() {
console.log(
'hello main'
);
}
};
});
|
<!--index.html-->
<
script
>
require(['main'], function(main) {
main.hello();
});
</
script
>
|
在本地測試,一般結果是這樣的:
require module: mod1
require module: mod2
require module: main
hello mod1
hello mod2
hello main
|
這個結果符合預期。可是這就是所有嗎?用 Fiddler 把 mod1.js 請求 delay 200
再測試,此次輸出:
require module: mod2
require module: mod1
require module: main
hello mod1
hello mod2
hello main
|
這是由於 main.js 中 mod1 和 mod2 兩個模塊並行加載,且加載完就執行,因此前兩行輸出順序取決於哪一個 js 先加載完。若是必定要讓 mod2 在 mod1 以後執行,須要在 define 模塊時申明依賴,或者經過 require.config 配置依賴:
require.config({
shim: {
'mod2'
: {
deps : [
'mod1'
]
}
}
});
|
嚴重問題!
咱們再回過頭來看 CommonJS Wrapper 會帶來什麼問題。前面說過,AMD 規範中,上面的 main.js
等價於這樣:
//main.js
define(
function
(require, exports, module) {
//運行至此,mod1.js 和 mod2.js 已經下載完成;
console.log(
'require module: main'
);
var
mod1 = require(
'./mod1'
);
//這裏才執行 mod1 ?
mod1.hello();
var
mod2 = require(
'./mod2'
);
//這裏才執行 mod2 ?
mod2.hello();
return
{
hello:
function
() {
console.log(
'hello main'
);
}
};
});
|
這種「就近」書寫的依賴,很是容易讓人認爲 main.js 執行到對應 require 語句時才執行 mod1 或 mod2,但這是錯誤的,由於 CommonJS Wrapper 並不會改變 AMD「儘早執行」依賴的本質!
實際上,對於按需執行依賴的加載器,如 SeaJS,上述代碼結果必定是:
require module: main
require module: mod1
hello mod1
require module: mod2
hello mod2
hello main
|
因而,瞭解過 CommonJS 或 CMD 模塊規範的同窗,看到使用 CommonJS Wrapper 方式寫的 AMD 模塊,容易產生理解誤差,從而誤認爲 RequireJS 有 bug。
我以爲「儘早執行」或「按需執行」兩種策略沒有明顯的優劣之分,但 AMD 這種「模仿別人寫法,卻提供不同的特性」這個作法十分愚蠢。這年頭,作本身最重要!
其餘問題
還有一個小問題也順帶提下:默認狀況下,定義 AMD 模塊時經過參數傳入依賴列表,簡單可依賴。而用了 CommonJS Wrapper 以後,RequireJS 須要經過正則從 factory.toString()
中提取依賴,複雜並容易出錯。如 RequireJS 下這段代碼會出錯:
define(
function
(require, exports, module) {
'/*'
;
var
mod1 = require(
'mod1'
),
mod2 = require(
'mod2'
);
'*/'
;
mod1.hello();
});
//Uncaught Error: Module name "mod1" has not been loaded yet for context: _
|
固然,這個由於 RequireJS 的正則沒寫好,把正常語句當註釋給過濾了,SeaJS 用的正則處理上述代碼沒問題,同時複雜了許多。
雖然實際項目中很難出現上面這樣的代碼,但若是放棄對腦殘的 CommonJS Wrapper 支持後,再寫 AMD 加載器就更加簡單可靠。例如雨夜帶刀同窗寫的 seed,代碼十分簡潔;構建工具一般基於字符串分析,仍然須要過濾註釋,但能夠採用 uglifyjs 壓縮等取巧的方法。
考慮到不是每一個 AMD Loader 都支持 CommonJS Wrapper,用參數定義依賴也能保證更好的模塊通用性。至於「就近」定義依賴,我一直以爲無關緊要,咱們寫 php 或 python 時,include 和 import 都會放在頂部,這樣看代碼時能一目瞭然地看到全部依賴,修改起來也方便。