[轉載]AMD 的 CommonJS wrapping

https://www.imququ.com/post/amd-simplified-commonjs-wrapping.htmlphp

它是什麼?

爲了複用已有的 CommonJS 模塊,AMD 規定了 Simplified CommonJS wrapping,而後 RequireJS 實現了它(前後順序不必定對)。它提供了相似於 CommonJS 的模塊定義方式,以下:html

 
define( function (require, exports, module) {
     var A = require( 'a' );
 
     return function () {};
});

這樣,模塊的依賴能夠像 CommonJS 同樣「就近定義」。但就是這個看上去一箭雙鵰的作法,給你們帶來了不少困擾。python

它作了什麼?

因爲 RequireJS 是最流行的 AMD 加載器,後續討論都基於 RequireJS 進行。git

直接看 RequireJS 這部分邏輯:github

 
//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 函數裏須要作這些事情:api

  1. 經過 factory.toString() 拿到 factory 的源碼;
  2. 去掉源碼中的註釋(避免匹配到註釋掉的依賴模塊);
  3. 經過正則匹配 require 的方式獲得依賴信息;

寫模塊時要把 require 當成保留字。模塊加載器和構建工具都要實現上述邏輯。瀏覽器

對於 RequireJS,本文最開始定義的模塊,最終會變成:bash

 
define([ 'a' ], function (require, exports, module) {
     var A = require( 'a' );
 
     return function () {};
});

等價於:app

 
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 都會放在頂部,這樣看代碼時能一目瞭然地看到全部依賴,修改起來也方便。

本文部分示例來自於 SeaJS 與 RequireJS 最大的區別,致謝!

相關文章
相關標籤/搜索