# 前端模塊化
主要介紹CommonJS模塊 和 ES6模塊的區別,不感興趣可以跳過本節內容。
## 1、ES6模塊
ES6 模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。
**1.1 CommonJs模塊運行時加載(對象),ES6模塊編譯時輸出接口**
引用[阮一峰老師的例子][1]來進行說明:
CommonJS的輸出緩存機制,使得模塊被加載時,會被緩存起來,將整個模塊當作一個對象輸出。
```js
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
```
上面代碼的實質是整體加載fs模塊(即加載fs的所有方法),生成一個對象(_fs),然后再從這個對象上面讀取 3 個方法。這種加載稱為“運行時加載”,因為只有運行時才能得到這個對象,導致完全沒辦法在編譯時做“靜態優化”。
并且,由于緩存機制,重復加載同一模塊,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結果,除非手動清除系統緩存。
ES6 模塊不是對象,而是通過export命令顯式指定輸出的代碼,再通過import命令輸入。
```js
// ES6模塊
import { stat, exists, readFile } from 'fs';
```
上面代碼的實質是從fs模塊加載3個方法,其他方法不加載。這種加載稱為“編譯時加載”或者靜態加載,即 ES6 可以在編譯時就完成模塊加載,效率要比CommonJS模塊的加載方式高。當然,這也導致了沒法引用 ES6 模塊本身,因為它不是對象。
**1.2 CommonJs 模塊輸出的是一個值的復制,ES6模塊編譯時輸出接口(動態只讀引用)**
CommonJs 模塊:
對于基本數據類型,屬于復制。即會被模塊緩存。同時,在另一個模塊可以對該模塊輸出的變量重新賦值,而不會影響其他模塊。
對于復雜數據類型,屬于淺拷貝。由于兩個模塊引用的對象指向同一個內存空間,因此對該模塊的值做修改時會影響另一個模塊。
ES6 模塊:
ES6 模塊的運行機制與 CommonJS不一樣。JS引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。
**1.3 對于循環引用的處理**
CommonJS 模塊:
其重要特性是加載時執行,即腳本代碼在require的時候,就會全部執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。
例如:
```js
// a.js
console.log('a start');
var b = require('./b.js');
console.log('a doing');
console.log(b);
exports.a = 'a模塊的變量';
// b.js
console.log('b start');
var a = require('./b');
console.log('b doing')
console.log(a);
exports.b = 'b模塊的變量';
// 要理解CommonJS模塊是同步執行
// 當執行 node a.js時,輸出為:
// a start
// b start
// b doing
// {}
// a doing
// {b: 'b模塊的變量'}
```
ES6 模塊:
ES6 模塊是動態引用,如果使用import從一個模塊加載變量,那些變量不會被緩存,而是成為一個指向被加載模塊的引用,需要開發者自己保證,真正取值的時候能夠取到值。
上面例子的ES6改寫:
```js
$ node –experimental-modules a.mjs
// a.js
console.log('a start');
import {b} from ./b.mjs';
console.log('a doing');
console.log(b);
exports.a = 'a模塊的變量';
// b.js
console.log('b start');
import {a} from ./a.mjs';
console.log('b doing')
console.log(a);
exports.b = 'b模塊的變量';
// 輸出
// b start
// b ding
// ReferenceError: a is not defined
```
分析:執行a.mjs以后,引擎發現它加載了b.mjs,因此會優先執行b.mjs,然后再執行a.mjs。接著,執行b.mjs的時候,已知它從a.mjs輸入了foo接口,這時不會去執行a.mjs,而是認為這個接口已經存在了,繼續往下執行。執行到第三行console.log(foo)的時候,才發現這個接口根本沒定義,因此報錯。
解決方法:
```js
// a.js
// exports.a = 'a模塊的變量'; 改為
export function a () {console.log('a模塊的函數')}
// b.js
// exports.b = 'b模塊的變量';
export function b () {console.log('b模塊的函數')}
// 輸出
// b start
// b ding
// [Function: a]
// a start
// b doing
// [Funcion: b]
```
分析:function a(){...}具有提升作用。因此,在import之前,已經存在函數a。
下面簡單介紹前端的其它模塊化規范。
## 2、CommonJS規范
1、核心思想:
> 把一個文件當做一個模塊,通過require方法同步加載模塊。
2、應用場景
> Node.js的服務端編程加載的模塊主要存在于服務器磁盤,所以加載速度較快,對node這種單線程,影響較小。因此,CommonJS同步加載模塊方案主要應用于**node**服務端。
3、不適用于瀏覽器環境
> 瀏覽器加載方式與服務端完全不同。如果瀏覽器請求服務器資源時,會由于某個請求加載的時間過長導致瀏覽器阻塞,不會往下執行,所以就會出現網頁打開緩慢的現象。并且,瀏覽器端是以插入<script>標簽的形式來加載資源(ajax方式不行,有跨域問題),沒辦法讓代碼同步執行,使用commonJS同步寫法會直接報錯。
4、示例
```js
// 1.js
module.export = function() {
say: function() {
alert("你好");
}
}
// main.js
var a = require('./a.js');
a.say();
```
瀏覽器環境下必須采用異步模式。所以就有了ES6模塊、 AMD,CMD 解決方案。
## 3、AMD規范
AMD(異步模塊定義)是RequireJS在推廣過程中對模塊化定義的規范化產出。
1、核心思想:
> 異步加載所需的模塊,然后在回調函數中執行主邏輯。
**可以并行加載多個模塊,等所有模塊都加載并且解釋執行完成后**,才會執行接下來的代碼,
2、吐槽點
> “提前執行”:所有依賴模塊會被預先下載,并且提前執行(不管該模塊的調用時機)。“提前執行”的性能消耗是不容忽視。
3、懶加載
> AMD保留了commonjs中的require、exprots、module這三個功能。因此,為了解決“提前執行”的性能消耗,依賴的模塊不寫在dependencies數組中,
4、示例
```js
// a.js
define(function(){
return {
say: function(){
console.log('hello, a.js');
}
}
});
// b.js
define(function(){
return {
say: function(){
console.log('hello, a.js');
}
}
});
// main.js
require(['a', 'b'], function(a, b){
a.say();
$('#b').click(function(){
b.say();
});
})
// AMD懶加載方式
// main.js
require(['a'], function(a){
a.say();
$('#b').click(function(){
require(['b'], function(b){
b.say();
});
});
})
```
這種懶加載減輕了初始化操作。**但是,在需要執行b.say\(\)時,需要實時下載代碼然后在回調中才能執行,用戶的操作會有明顯的延遲卡頓。**
## 4、CMD規范
CMD\(同步模塊定義\)是SeaJS\(淘寶團隊\)在推廣過程中對模塊化定義的規范化產。
1、示例
```js
// a.js
define(function(require, exports, module){
return {
say: function(){
console.log('hello, a.js');
}
}
});
// b.js
define(function(require, exports, module){
return {
say: function(){
console.log('hello, b.js');
}
}
});
// main.js
define(function(require, exports, module){
var a = require('a');
a.say();
$('#b').click(function(){
var a = require('b');
b.say();
});
});
})
```
a.js和b.js都會預先下載,但是,**b.js不會預先執行(即用即返回)。**
[1]: http://es6.ruanyifeng.com/#docs/module-loader