CommonJS
、
AMD
、
CMD
、
UMD
、
ES6 Module
,而後針對
CommonJS
和
ES6 Module
進行深刻闡述
在日常開發的過程當中,全部的這些代碼都是由一個個的模塊構成的,咱們能夠在npm上下載咱們須要的包,而後組裝在咱們的項目中,就好像組裝模塊同樣,隨着mvvm框架的普及,主流的開發方式都變成了模塊化。在以前,js模塊化仍是經過命名空間來實現的,再後來產生了一個模塊化規範(CommonJs
),CommonJs
是誕生於node社區,但它只能在服務端運行,在瀏覽器端仍是用不了,因而,社區便出了AMD
規範,國內又出了CMD
,後來又出了UMD
,其實這三個規範都是爲了幫助模塊化開發。目前比較流行的是ES6 Module
規範。html
CommonJS
規範的特色:前端
一個文件即一個模塊。文件中定義的變量、函數、類都是私有的,外界沒法訪問模塊內的內容node
經過module.exports
暴露模塊內的常量、函數、文件、模塊等webpack
module.exports
導出模塊,輸出的是值的拷貝;模塊導入的也是輸出值的拷貝git
使用require
引入模塊es6
同步加載:github
CommonJS
是運行在Nodejs
服務端的,Node.js
是同步模塊加載,所以CommonJS
是用同步的方式加載模塊,對服務端來講require()
是本地加載,正由於全部文件都在本地,讀取速度很快,同步加載不會形成什麼很差的影響,可是在瀏覽器端,由於網絡的緣由,同步加載就會在必定程度限制資源的加載速度模塊是運行時加載(運行時加載)web
CommonJS
規範中,require()
是用來加載一個模塊,那require
這語句作了什麼事?npm
運行文件時,Node
發現文件中使用require
加載一個A模塊,Node
首先會執行整個A模塊,而後在內存中生成一個對象,該對象就是A模塊的一個表達載體,接着從該對象的exports
屬性中取出A模塊輸出的各個接口,供當前文件使用api
{
id: '', // 惟一的模塊名
exports: { // 包含模塊輸出的各個接口
...
},
loaded: true, // 模塊的腳本是否執行完畢
...
}
複製代碼
若在文件中再次使用require
加載A模塊,Node
也不會重複執行A模塊,而是直接在內存中找到A模塊的表達對象,取出exports
屬性的值。也就是說不管經過require
加載多少次相同的模塊,都只會在第一次加載中運行,日後都是從對象中取出exports
屬性的值(除非手動清除系統緩存)
而後能夠看一下CommonJS
規範的代碼
var foo = require('./foo') // 引入模塊
var foo = require('event').some // 引入模塊的某個屬性
function fn() {
console.log('fn')
}
module.exports.fn = fn // 經過module.exports將fn函數暴露出去
// exports.fn = fn // 或者經過exports暴露出去
複製代碼
AMD
是異步模塊加載機制,做者以RequireJS
實現了AMD
規範,因此提及AMD
規範就能想到RequireJS
,它的特色:
使用define定義模塊: define
能夠傳入三個參數,分別是模塊名(string
)、依賴模塊(array
)、回調函數(function
)
第一個參數是模塊名,字符串類型,可選參數。若不存在則模塊標識應該默認定義爲在加載器中被請求腳本的標識。若是存在,那麼模塊標識必須爲頂層的或者一個絕對的標識
第二個參數是依賴模塊,數組類型,是一個當前模塊依賴的,已被模塊定義的模塊標識的數組字面量
第三個參數是一個須要進行實例化的函數或者一個對象
// 定義無依賴的模塊
define({
fn1: function(a){
return a
},
arr1: []
})
// 定義有依賴的模塊
define(["a"], function(a){
return {
fn1: function(){
return a.num
}
}
})
// 具名模塊
define("foo", [ "a", "b"], function(a, b){
...
});
複製代碼
使用require
引入模塊:
異步加載模塊:模塊的加載不影響它後面語句的運行,全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行
依賴前置,提早執行:
define
方法裏傳入的依賴模塊(數組),會在一開始就下載並執行RequireJS
從2.0開始,也改爲能夠延遲執行,只是寫法上和提早執行有些不一樣。雖然 AMD
也支持CMD
寫法,但依賴前置是官方文檔的默認模塊定義寫法而後能夠看一下AMD
規範的代碼
// src/foo.js
// 會在聲明並初始化要用到的全部模塊(a和b),無論後續是否用到
// 參數1 "foo" 是模塊名
// 參數2 ["a", "b"] 是依賴的模塊名
// 參數3 function(){} 是回調函數
define("foo", ["a", "b"], function (a, b) { // 定義foo.js模塊
const NUM = 2
const fn = function () {
console.log('foo')
}
return {
NUM: NUM,
fn: fn
}
})
// app.js
require.config({ // 經過require.config()設置每一個模塊路徑和引用名
baseUrl: "src",
paths: {
"foo": "foo", // 實際路徑爲src/foo.js, 指定foo.js的引用名爲foo
}
})
// 加載foo.js模塊
// 加載模塊時要將模塊名(也能夠是文件路徑)放在[]中做爲reqiure()的第一參數,
require(["foo"],function(foo){
console.log(foo)
})
複製代碼
CMD
是通用模塊定義,CMD
是SeaJS
在推廣過程當中生產的對模塊定義的規範,SeaJS
的做者就是大名鼎鼎的玉伯,它的特色:
require()
時,依賴模塊才執行。AMD
和CMD
其實不少地方類似,可是它們最大區別是執行方式的不一樣,在執行過程當中,通過AMD
編譯後,全部require
引入的模塊都被前置了,CMD
雖然也會將require
的代碼下載下來,可是它不會去執行,直到代碼運行到那個模塊的依賴纔會去執行對應模塊
/** AMD寫法 **/
define(['./a','./b'], function (a, b) {
//依賴一開始就寫好,等於在最前面聲明並初始化了要用到的全部模塊
a.test() // 即使沒用到某個模塊 b,但 b 仍是提早執行了
})
/** CMD寫法 **/
define(function (requie, exports, module) {
if (false) {
var b = require('./b') //依賴能夠在須要時申明
b.doSomething()
}
exports.fn = function() {
console.log('fn')
};
})
複製代碼
👇是sea.js
的使用demo
//a.js
define(function (require, export, module)) {
// 經過require引入依賴
var foo = require('./foo')
const NUM = 2
// 經過export或者經過module.exports暴露接口
exports.num = NUM
// module.exports.num = NUM
}
//b.js
// 加載模塊
// 數組中聲明須要加載的模塊,能夠是模塊名、js文件路徑
seajs.use(['./a'], function(a) {
a.fn();
});
複製代碼
在webpack
打包過程當中就有UMD
這個選項,UMD
實際上是一個通用解決方案,它自己不是什麼新的規範,就好似一個判斷條件同樣。在模塊定義中,它主要作了三件事
AMD
。若是是則使用AMD
,不然執行下一步CommonJs
。若是是則使用CommonJs
,不然執行下一步(function (root, fac) {
if (typeof define === 'function' && define.amd) { // 若是define這個方法被定義,且define.amd是否存在,說明是AMD
define([], fac) // 以AMD的規範去定義模塊
} else if (typeof exports === 'object'){ // exports爲一個對象,這個就是判斷是否在node環境中,知足Commonjs規範
module.exports = fac() // 以commonjs的規範去暴露接口
} else {
root.some = fac() // 暴露給瀏覽器的全局環境,root其實就是window
}
})
複製代碼
ES6 在語言標準的層面上,實現了模塊功能,並且實現得至關簡單,旨在成爲瀏覽器和服務器通用的模塊解決方案。webpack3
原生支持
一個文件爲一個模塊
使用export
暴露模塊內的常量、函數、文件、模塊等,暴露的是對值的引用
ES6 Module
暴露出去的是一種靜態定義(引用)export
命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
使用import引入模塊
ES6 Module
模塊對導出模塊,變量,對象是動態引用,遇到模塊加載命令import
時不會去執行模塊,只是生成一個指向被加載模塊的引用,不存在緩存值的問題。import
引入的值自己也會相應的變化(在後面循環加載章節會詳細闡述)模塊是編譯時輸出接口(編譯時加載)
ES6 Module
暴露出去的是一種靜態定義(引用),在代碼靜態解析階段就會生成
import/export
最終都是編譯爲require/exports
來執行的
// foo.js 暴露接口
export const NUM = 12
function fn() {
console.log('fn')
}
export { fn }
export default class myClass {...} // 默認暴露
// app.js 引入模塊
import myClass, { fn, NUM } from 'other_module'
複製代碼
目前
webpack
支持AMD(requireJS)
、ES6 Module
(官方推薦)、CommonJS
require
: node 和 es6 都支持的引入export / import
: 只有es6 支持的導出引入module.exports / exports
: 只有 node 支持的導出
CommonJS
規範中認爲一個文件即一個模塊
CommonJS
是運行在Nodejs服務端的,Node爲每一個模塊都建立一個module
對象以表明當前模塊,初始值爲{},該對象的exports
屬性(即module.exports
)是對外的接口,當經過**require
加載某個文件時,其實就是加載該文件的module.exports
**。下面代碼經過module.exports
輸出變量el和函數fn1。
// foo1.js
const el = 5
const fn1 = function (val) {
return `val: ${val}`
}
module.exports.el = el
module.exports.fn1 = fn1
複製代碼
// index.js
var foo1 = require('./foo1') // 加載模塊
console.log(foo1) // { a: 5, fn1: [Function: fn1] }
console.log(foo1.el) // 5
console.log(foo1.fn1(4)) // val: 4
複製代碼
前面說了,require
加載某個文件時,其實就是加載該文件的module.exports
,那exports
對象是什麼?
爲了方便,Node
爲每一個模塊提供一個exports
對象,並讓exports
對象指向module.exports
,如此一來,exports
和module.exports
都指向同一塊內存區域,這樣咱們就能夠直接在exports
對象上添加變量、函數、類等,以表示對外輸出的接口,如同添加在module.exports
同樣。其做用就像👇這行代碼同樣,
var exports = module.exports;
複製代碼
來看一下經過exports暴露接口的代碼:
// foo1.js
const el = 5
const fn1 = function (val) {
return `val: ${val}`
}
// 與module.exports上方寫法相同效果
exports.el = el
exports.fn1 = fn1
複製代碼
可是須要注意的是,不能直接將
exports
變量指向一個值(直接被覆蓋取值 例如:export = xxx),由於這樣等於切斷了exports
與module.exports
的聯繫;也不能修改module.exports
值,這樣也會讓exports
切斷聯繫
掏個🌰感覺下這句話的意思
// index.js
var foo1 = require('./foo1');
console.log(foo1)
// fool1.js
// eg1: 在第一次賦值以後,若是單獨修改module.exports指向,export仍是停留在上一次賦值的值,而且此時經過exports暴露任何變量、函數等是沒有用的,由於export已經不指向module.exports了
var exports = module.exports = {a: 1}
module.exports = {a: 2} // 此時exports值爲{a: 1}
const el = 5
exports.el = el // index.js輸出的值:{a: 2}
// fool1.js
// eg2:修改exports值,一樣致使export斷開與module.exports的聯繫
var exports = module.exports
const el = 5
exports = {} // export指向新的對象
exports.el = el // index.js輸出的值:{},由於module.exports默認是{}
複製代碼
從上面能夠看出,require
永遠只會導出module.exports
的內容,exports
就好似一個代駕,當斷開與module.exports
的聯繫,他就沒用了,它的做用就是方便用戶使用module.exports
這個對象
總結一下exports
和 module.exports
的區別了:
module.exports
初始值爲一個空對象 {}exports
是指向的 module.exports
的引用require()
返回的值是被加載文件的module.exports
若是有人有疑問,既然是引用關係,爲啥修改exports
不會同時修改module.exports
的值?能夠看看👇代碼理解下引用的意思
var a = {name: 'one'}
var b = a
console.log(a) // {name: 'one'}
console.log(b) // {name: 'one'}
b.name = 'two'
console.log(a) // {name: 'two'}
console.log(b) // {name: 'two'}
var b = {name: 'three'}
console.log(a) // {name: 'two'}
console.log(b) // {name: 'three'}
複製代碼
a 是一個對象,b 是對 a 的引用,即 a 和 b 指向同一塊內存,因此前兩個輸出同樣。當對 b 做修改時,即 a 和 b 指向同一塊內存地址的內容發生了改變,因此 a 也會體現出來,因此第三四個輸出同樣。當 b 被覆蓋時,b 指向了一塊新的內存,a 仍是指向原來的內存,因此最後兩個輸出不同。
ES6 module
是經過export
和export default
來暴露接口,那它們的聯繫和區別是什麼?
先說明export
和export default
的區別:
在一個文件或模塊中,export
能夠有多個,export default
僅有一個
經過export
方式導出,在導入時要加{ },export default
則不須要
export
可直接導出變量表達式,export default
不可
// foo.js
// export導出
export const NUM1 = 1 // 直接導出
function fn () {
console.log('res')
}
const NUM2 = 2
export { fn, NUM2 } // 間接導出
// export default導出
export default function fn1 () { // 直接導出
console.log('res1')
}
// const NUM2 = 2
// export default NUM2 // 間接導出
// export defult const NUM2 = 2 // export default不可導出變量表達式
複製代碼
// index.js
import { NUM1, fn, NUM2 } from './foo' // 導出了export
import fn1 from './foo' // 導出了export default
// import fn1, { NUM1, fn, NUM2 } from './foo' // 等價於上面兩行
import * as all from './foo' // 將export和export default暴露出的接口都用對象all表示
fn1() // res1
fn() // res
console.log(NUM1) // 1
console.log(NUM2) // 2
console.log(all.NUM) // 1
console.log(all.default) // res1 export default暴露出來的在all的default屬性裏
複製代碼
另外提一下,暴露的方式還有一些拓展的寫法
// 第二種方式:先定義,後暴露
const NUM = 12
function fn() {...}
export { NUM, fn } // 暴露上面定義的NUM變量和fn函數
export { NUM as NUM_NEW, fn as fn1 } // 以NUM_NEW代替NUM,被暴露出去;fn同理,import也要用NUM_NEW和fn1來引入
// 能夠將其餘模塊的內容引入進來,而後再暴露出去,做一箇中轉站
export {foo, bar} from 'other_module' // 引入foo和bar接口,並將其暴露出去
export * from 'src/other_module' // 表示先引入other_module模塊的全部接口,而後所有暴露出去
export {foo as foo1, bar} from 'other_module' // 也能夠進行重命名,將foo以foo1的名字暴露出去
複製代碼
「循環加載」指的是,a
腳本的執行依賴b
腳本,而b
腳本的執行又依賴a
腳本
// a.js
var b = require('b');
// b.js
var a = require('a');
複製代碼
循環加載最容易發生的狀況就是遞歸加載,致使無限循環,可是複雜項目中模塊衆多,模塊互相依賴的狀況也時常發生,所以,主流的兩個模塊規範CommonJS
和ES6 Module
也給出了各自的解決方案
前面討論過CommonJS
的加載原理,require
第一次加載模塊會生成一個對象,此後重複加載該模塊都是從對象中的exports
屬性中取值。針對循環加載
👉
CommonJS
的方案是:一旦出現某個模塊被「循環加載」,就只輸出已經執行的部分,沒有執行的部分不會輸出。
結合🌰進行理解,下面🌰來自於Node官方文檔
//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);
複製代碼
//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執行完畢!')
複製代碼
node main.js // 運行main.js
輸出結果:
在b.js中,a.done = false
b.js執行完畢!
在a.js中,b.done = true
a.js執行完畢!
在main.js中,a.done = true, b.done = true
複製代碼
exports
屬性中取值,done=false,而後繼續往下執行,輸出在b.js中,a.done = false
,而後輸出b.js執行完畢!
在a.js中,b.done = true
,而後輸出a.js執行完畢!
exports
屬性前面介紹了ES6 Module
兩個特色:
一、使用
export
暴露接口,暴露的是對值的引用二、使用
import
動態引入模塊,遇到import時不會去執行模塊,只是生成一個指向被加載模塊的引用,等到真的須要用到模塊時,再到模塊裏面去取值。
舉個🌰,例子來源於阮一峯
// a.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// b.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
// 執行b.js,輸出爲:
bar
baz
複製代碼
從結果能夠看出,a.js的變量foo,在剛加載時等於bar,過了500毫秒,又變爲等於baz,b.js一開始獲取到foo的值時bar,500毫秒後獲取的值時baz,這就說明ES6 Module
不會去緩存運行結果,而是動態地去取被加載模塊暴露的值,由於import
生成的引用,其實就是一個地址引用,指向那塊數據的內存
👉
ES6 Module
不會關心import是否發生」循環加載「,由於它僅僅生成一個引用,須要開發者本身保證,在文件中真正使用被加載模塊時,引用是能夠取到值
也就是說,在文件中使用import
加載A模塊時,僅僅是生成一個引用(相似指針),該引用指向的是A模塊暴露接口的地址,所以,開發者必須保證在真正使用A模塊時,是能夠取到A模塊的值,不然就會報錯
// a.js
import {resB} from './b'
console.log('a')
export const resA = 2
// b.js
import {resA} from './a'
console.log('b')
export const resB = 3
console.log(resA)
// 輸出結果
b
undefined
a
複製代碼
上述代碼,a.js加載b.js,b.js又加載a.js,構成了循環加載。按照CommonJS
規範,上面的代碼是無法執行的。a
先加載b
,而後b
又加載a
,這時a
尚未任何執行結果,因此輸出結果爲null
,即對於b.js
來講,變量resA的值等於null
,後面的foo()
就會報錯。
讓咱們一行行來看,ES6 循環加載是怎麼處理的。首先,執行a.js之後,引擎發現它加載了b.js,所以會優先執行b.js,而後再執行a.js。接着,執行b.js的時候,已知它從a.js輸入了resA接口,這時不會去執行a.js,而是認爲這個接口已經存在了,繼續往下執行,執行到console.log(resA)時,發現這個接口沒有定義,而後輸出undefined。接着繼續執行a.js,輸出a
補充知識點:在代碼運行前,函數聲明和變量定義一般會被解釋器移動到其所在做用域的最頂部。若是把函數寫成函數表達式,就不具備提高做用了
那如何解決這個問題?能夠利用函數提高來解決,在a.js中經過函數包裹NUM常量,而後將該值進行返回,因爲函數會被提高,因此當b.js執行resA()的時候,a.js的resA已經被聲明瞭,此時執行console.log(resA())就能獲得正常的結果
// a.js
import {resB} from './b'
console.log('a')
function resA() {
const NUM = 2
return NUM
}
export { resA }
// b.js
import {resA} from './a'
console.log('b')
export const Foo2 = 3
console.log(resA())
// 輸出結果
b
2
a
複製代碼
CommonJS
暴露出的是一個值的拷貝,一旦暴露接口,這個接口在模塊內部的變化是監聽不到的;ES6 Module
暴露的是內容的引用,模塊內部被暴露的接口改變會影響引用的改變CommonJS
會直接從第一次加載時生成對象的exports
屬性中取值;ES6 Module
則會經過引用找到模塊暴露接口的內存位置,並從中取值CommonJS
只輸出已經執行的部分,還未執行的部分不會輸出;ES6 Modul
e須要開發者本身保證,真正取值的時候可以取到值CommonJS
是加載時執行,若出現循環加載狀況,則從已執行的內容中取值;ES6 Module
是動態引用,加載模塊時不執行代碼,只是生成一個指向被加載模塊的引用CommonJS
模塊是運行時加載,ES6 Module
模塊是編譯時輸出接口CommonJS
是加載整個模塊,ES6 Module
能夠按需加載部分接口但願看完本篇文章能對你有所幫助,
文中若有錯誤,歡迎在評論區指正,若是這篇文章幫助到了你,歡迎點贊和關注。