JS模塊之AMD, CMD, CommonJS、UMD和ES6模塊

CommonJS

傳送門javascript

同步加載,適合服務器開發,node實現了commonJS。module.exports和requirecss

判斷commonJS環境的方式是(參考jquery源碼):html

if ( typeof module === "object" && typeof module.exports === "object" ) 

 一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。java

// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');

// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');

// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

// 運行結果
在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true
View Code

 

對於commonJs模塊的運行理解:node

  模塊中的代碼運行在一個函數中,全部根上的變量都存在於當前做用域中,而後返回一個導出對象,導出對象中的函數引用的都是當前做用域內的變量。 jquery

UMD

UMD是AMD和CommonJS的糅合git

AMD模塊以瀏覽器第一的原則發展,異步加載模塊。
CommonJS模塊以服務器第一原則發展,選擇同步加載,它的模塊無需包裝(unwrapped modules)。
這迫令人們又想出另外一個更通用的模式UMD (Universal Module Definition)。但願解決跨平臺的解決方案。es6

UMD先判斷是否支持Node.js的模塊(exports)是否存在,存在則使用Node.js模塊模式。
在判斷是否支持AMD(define是否存在),存在則使用AMD方式加載模塊。github

ES6模塊

  模塊內默認開啓嚴格模式,可用於代替AMD和commonJs,也就是說再也不需要UMD了。ES6中對於AMD異步加載使用的是import函數,而commonJs同步加載對應ES6中import關鍵字正則表達式

  export和import要求必須在頂層做用域中,import在靜態編譯階段執行,不能使用運行時的值。但import()函數能夠,他異步加載返回一個promise,能運行在任意地方,能夠實現按需下載

和commonJs的差別

  commonJs輸出的是一個對象;而ES6模塊輸出的不是對象,而是一個只讀地址引用的集合。前者在生成對象返回的時候,會對對象中要用到的值進行拷貝進對象中,這很常規容易理解,然後者輸出的是地址,因此在運行時會動態地到模塊的做用域中讀取值,沒有「拷貝」這種說法,若是對es6導出的值進行賦值,報錯:

Uncaught TypeError: Assignment to constant variable.

但能夠調用導出函數,函數內對模塊內的值進行修改。

  由於es6模塊默認是嚴格模式,因此頂層this爲undefined,而commonJs中頂層this指向當前模塊

 

不一樣地方屢次導入同一個模塊,模塊只會執行一次:

// x.js
import {c} from './mod';
c.add();  // 0 -> 1

// y.js
import {c} from './mod';
c.show(); //1

// main.js
import './x';
import './y';

//運行main 輸出 1

以上不一樣模塊中都導入了一次mod模塊,而獲取到的導出對象是相同的,說明mod模塊只運行了一次。

循環引用

   當模塊被執行的時候,會首先執行被提高了的語句(functnion、export),在執行import,最後再執行內部的賦值語句、邏輯等。

  出現循環引用時,只要被引入模塊已經運行完還沒運行完,就不會再次運行。a->b->a,b中的a就不會再次運行。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

// 運行a
b.mjs
ReferenceError: foo is not defined

把a中foo的賦值改成函數定義,則不會報錯,由於函數定義有提高。

babel的轉換原理

export { NormalExport }
export { RenameExport as HasRenamed }
export default DefaultExport

// 轉換爲
exports.NormalExport = NormalExport;
exports.HasRenamed = RenameExport;
exports.default = DefaultExport;

//-------------------------------------------

import { NormalExport } from 'normal'

// 轉換爲
var _normal = require('normal');

可見,import關鍵字是同步的。

Chrome 61 加入了對 JavaScript Module <script type="module"> 的原生支持,後續介紹如下這個特性的用法:

參考:http://es6.ruanyifeng.com/#docs/module-loader#加載規則

在script中加入type=module,意味着這個腳本支持ES6模塊的語法。

<script type="module">
        import {x} from './es6module.js';
        alert(x);
</script>

//./es6module.js
export var x = "123";

以上能正常運行。若是去掉type,則報錯:Uncaught SyntaxError: Unexpected token import

加了這個屬性,兩段腳本就運行在不一樣的環境中了,如下運行報錯 ReferenceError: x is not defined

    <script type="module">
        var x = 123
    </script>
    <script>
        alert(x);
    </script>

總結ES6模塊和commonJs的對比:

  1. 導出的機制不一樣,前者是隻讀的地址,然後者是一個對象
  2. 頂層this的指向不一樣
  3. 循環引用時,都是遇到被加載的模塊正在運行或者已經運行完,就再也不進去運行,模塊沒運行完輸出的都是已運行部分。
  4. es6靜態編譯,import關鍵字不容許使用動態值,而commonJs的require是動態執行,能夠讀取動態值

AMD

可實現異步的模塊加載。適合瀏覽器,RequireJS庫實現了AMD規範  傳送門

關鍵詞:define(factory)、require(['xxx'],fn)  

補充:單文件多模塊

2.js

define('a',function(){
    return { v:"a" } }); define('b',function(){ return { v:"b" } });

main.js

require.config({
    paths: {
        'a': "2", 'b': "2", } }); require(['a','b'], function(a,b) { alert(a.v); alert(b.v); });

可見,paths中的名字不是隨便寫的,必須和模塊文件內定義的名字要一致

 

測試2:屢次引入同一個模塊

2.js

define('a',function(){
    return { v:"a" } }); console.log('module a exec')

main.js

require.config({
    paths: {
        'a': "2", } }); require(['a'], function(a) { a.x = 789; require(['a' + ""], function(a) { console.log(a.x); }); console.log(123) });

輸出

屢次引入只會執行一次模塊,並且使用的都是同一個模塊對象,雖然第二次引入的模塊在第一次就下載好了,但回調的代碼依然會在下一輪事件循環中執行

模塊的名字能夠是表達式,如以上的['a'] 能夠寫成['a'+""],並且僅當require被執行的時候,模塊纔會開始下載,下載完再執行模塊,而後觸發回調函數。

實際作的事情:

  1. require函數檢查依賴的模塊,根據配置文件,獲取js文件的實際路徑
  2. 根據js文件實際路徑,在dom中插入script節點,並綁定onload事件來獲取該模塊加載完成的通知。
  3. 依賴script所有加載完成後,調用回調函數

CMD

傳送門

seaJS實現了這個規範。分析階段(使用正則表達式進行依賴的抓取),會把全部require的文件都下載好了(這一步可能會致使程序剛開始響應比較慢),再執行入口模塊

因此咱們使用的時候寫 var a = require('a') 雖然是引入一個模塊,但能夠認爲這句代碼沒有任何阻塞,由於模塊已經提早下載好了

 

 測試代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event Propagation</title>
    <script src="https://cdn.bootcss.com/seajs/3.0.2/sea.js"></script>
    <script>
        seajs.use("./1.js")
    </script>
</head>
<body>
</body>
</html>

// 1.js
define(function(require){
    console.log("main module exec")
    setTimeout(function(){
        var exec = Math.random()*100%2;
        if(exec > 1){
            let two = require("./2.js");
            let two1 = require("./2.js");
            two.show();
        }
    },1000)
});

// 2.js
define(function(require,exports,module){
    console.log("module two exec")
    module.exports = {
        show :function(){
            console.log("show in module two exec")
        }
    }
})

 運行結果:

結論:

  seajs在分析階段會下載全部的依賴而無論這些依賴是否會被真正執行,依賴的名字能夠使用變量,依賴都下載好了才執行入口模塊,依賴在第一次被require的時候才執行

 

實際作的事情是:

  1. 經過回調函數的Function.toString函數,使用正則表達式來捕捉內部的require字段,找到require('jquery')內部依賴的模塊jquery
  2. 根據配置文件,找到jquery的js文件的實際路徑
  3. 在dom中插入script標籤,載入模塊指定的js,綁定加載完成的事件,使得加載完成後將js文件綁定到require模塊指定的id(這裏就是jquery這個字符串)上
  4. 回調函數內部依賴的js所有加載(暫不調用)完後,調用回調函數
  5. 當回調函數調用require('jquery'),即執行綁定在'jquery'這個id上的js文件,即刻執行,並將返回值傳給var b
可見以上依賴搜索這一塊用正則表達式搜索,致使模塊名只能硬編碼,而不能運算和使用變量

rj和sj的區別

  https://github.com/seajs/seajs/issues/277

  https://www.zhihu.com/question/20351507

  https://www.zhihu.com/question/20342350

  都是異步下載模塊。

  rj會按需下載,下載完立刻執行,不能保證多個依賴是按順序執行的,次序不可控,玉伯認爲這是一個坑,可是amd規定中歷來沒規定過模塊的執行次序,異步模塊之間能夠沒有前後,但被依賴的異步模塊必須先於當前模塊執行

  sj會先進行分析,而後下載好全部依賴,再開始執行入口模塊(這可能會致使程序響應比較慢);在模塊第一次被require的時候,模塊纔會執行,因此能夠保證模塊的執行次序和require的次序是一致的,但這可能致使一個問題,模塊被require的時候執行,萬一內部出錯了,當前模塊該怎麼辦?被依賴的模塊有問題,當前模塊執行有何意義?

  對於cmd,一require就能夠立刻使用,給人的感受就像是同步代碼,而amd邏輯是寫在回調函數中的,給人異步的感受

相關文章
相關標籤/搜索