其實本文的標題應該是「爲何我不推薦使用 AMD 的 Simplified CommonJS wrapping」,但太長了很差看,爲了美觀我只能砍掉一截。javascript
爲了複用已有的 CommonJS 模塊,AMD 規定了 Simplified CommonJS wrapping,而後 RequireJS 實現了它(前後順序不必定對)。它提供了相似於 CommonJS 的模塊定義方式,以下:php
JSdefine(function(require, exports, module) { var A = require('a'); return function () {}; });
這樣,模塊的依賴能夠像 CommonJS 同樣「就近定義」。但就是這個看上去一箭雙鵰的作法,給你們帶來了不少困擾。html
因爲 RequireJS 是最流行的 AMD 加載器,後續討論都基於 RequireJS 進行。java
直接看 RequireJS 這部分邏輯:python
JS//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 函數裏須要作這些事情:git
factory.toString()
拿到 factory 的源碼;require
的方式獲得依賴信息;寫模塊時要把 require
當成保留字。模塊加載器和構建工具都要實現上述邏輯。github
對於 RequireJS,本文最開始定義的模塊,最終會變成:api
JSdefine(['a'], function(require, exports, module) { var A = require('a'); return function () {}; });
等價於:瀏覽器
JSdefine(['a'], function(A) { return function () {}; });
結論是,CommonJS Wrapper 只是書寫上兼容了 CommonJS 的寫法,模塊運行邏輯並不會改變。bash
AMD 運行時核心思想是「Early Executing」,也就是提早執行依賴。這個好理解:
JS//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 這個特性後,再來看一段代碼:
JS//mod1.js define(function() { console.log('require module: mod1'); return { hello: function() { console.log("hello mod1"); } }; });
JS//mod2.js define(function() { console.log('require module: mod2'); return { hello: function() { console.log("hello mod2"); } }; });
JS//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'); } }; });
HTML<!--index.html--> <script> require(['main'], function(main) { main.hello(); }); </script>
在本地測試,一般結果是這樣的:
BASHrequire module: mod1 require module: mod2 require module: main hello mod1 hello mod2 hello main
這個結果符合預期。可是這就是所有嗎?用 Fiddler 把 mod1.js 請求 delay 200
再測試,此次輸出:
BASHrequire module: mod2 require module: mod1 require module: main hello mod1 hello mod2 hello main
這是由於 main.js 中 mod1 和 mod2 兩個模塊並行加載,且加載完就執行,因此前兩行輸出順序取決於哪一個 js 先加載完。若是必定要讓 mod2 在 mod1 以後執行,須要在 define 模塊時申明依賴,或者經過 require.config 配置依賴:
JSrequire.config({ shim: { 'mod2': { deps : ['mod1'] } } });
咱們再回過頭來看 CommonJS Wrapper 會帶來什麼問題。前面說過,AMD 規範中,上面的 main.js
等價於這樣:
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,上述代碼結果必定是:
BASHrequire 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 下這段代碼會出錯:
JSdefine(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 都會放在頂部,這樣看代碼時能一目瞭然地看到全部依賴,修改起來也方便。
本文部分示例來自於 SeaJS 與 RequireJS 最大的區別,致謝!
本文連接:https://imququ.com/post/amd-simplified-commonjs-wrapping.html,參與評論 »
--EOF--