Javascript模塊化編程,已經成爲一個迫切的需求。理想狀況下,開發者只須要實現核心的業務邏輯,其餘均可以加載別人已經寫好的模塊。javascript
Javascript社區作了不少努力,在現有的運行環境中,實現」模塊」的效果。css
CommonJS定義的模塊分爲: 模塊引用(require) 模塊輸出(exports) 模塊標識(module)html
CommonJS Modules有1.0、1.一、1.1.1三個版本:前端
Node.js、SproutCore實現了 Modules 1.0java
SeaJS、AvocadoDB、CouchDB等實現了Modules 1.1.1node
SeaJS、FlyScript實現了Modules/Wrappingsjquery
這裏的CommonJS規範指的是CommonJS Modules/1.0規範。webpack
CommonJS是一個更偏向於服務器端的規範。NodeJS採用了這個規範。CommonJS的一個模塊就是一個腳本文件。require命令第一次加載該腳本時就會執行整個腳本,而後在內存中生成一個對象。es6
{ id: '...', exports: { ... }, loaded: true, ... }
id是模塊名,exports是該模塊導出的接口,loaded表示模塊是否加載完畢。此外還有不少屬性,這裏省略了。web
之後須要用到這個模塊時,就會到exports屬性上取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存中取值。
// math.js exports.add = function(a, b) { return a + b; }
var math = require('math'); math.add(2, 3); // 512
因爲CommonJS是同步加載模塊,這對於服務器端不是一個問題,由於全部的模塊都放在本地硬盤。等待模塊時間就是硬盤讀取文件時間,很小。可是,對於瀏覽器而言,它須要從服務器加載模塊,涉及到網速,代理等緣由,一旦等待時間過長,瀏覽器處於」假死」狀態。
因此在瀏覽器端,不適合於CommonJS規範。因此在瀏覽器端又出現了一個規範—AMD(AMD是RequireJs在推廣過程當中對模塊定義的規範化產出)。
CommonJS解決了模塊化的問題,但這種同步加載方式並不適合於瀏覽器端。
AMD是」Asynchronous Module Definition」的縮寫,即」異步模塊定義」。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。
這裏異步指的是不堵塞瀏覽器其餘任務(dom構建,css渲染等),而加載內部是同步的(加載完模塊後當即執行回調)。
AMD也採用require命令加載模塊,可是不一樣於CommonJS,它要求兩個參數:
require([module], callback);1
第一個參數[module],是一個數組,裏面的成員是要加載的模塊,callback是加載完成後的回調函數。若是將上述的代碼改爲AMD方式:
require(['math'], function(math) { math.add(2, 3); })
其中,回調函數中參數對應數組中的成員(模塊)。
requireJS加載模塊,採用的是AMD規範。也就是說,模塊必須按照AMD規定的方式來寫。
具體來講,就是模塊書寫必須使用特定的define()函數來定義。若是一個模塊不依賴其餘模塊,那麼能夠直接寫在define()函數之中。
define(id?, dependencies?, factory);12
id:模塊的名字,若是沒有提供該參數,模塊的名字應該默認爲模塊加載器請求的指定腳本的名字;
dependencies:模塊的依賴,已被模塊定義的模塊標識的數組字面量。依賴參數是可選的,若是忽略此參數,它應該默認爲 ["require", "exports", "module"]
。然而,若是工廠方法的長度屬性小於3,加載器會選擇以函數的長度屬性指定的參數個數調用工廠方法。
factory:模塊的工廠函數,模塊初始化要執行的函數或對象。若是爲函數,它應該只被執行一次。若是是對象,此對象應該爲模塊的輸出值。
假定如今有一個math.js文件,定義了一個math模塊。那麼,math.js書寫方式以下:
// math.js define(function() { var add = function(x, y) { return x + y; } return { add: add } })
加載方法以下:
// main.js require(['math'], function(math) { alert(math.add(1, 1)); })
若是math模塊還依賴其餘模塊,寫法以下:
// math.js define(['dependenceModule'], function(dependenceModule) { // ... })
當require()函數加載math模塊的時候,就會先加載dependenceModule模塊。當有多個依賴時,就將全部的依賴都寫在define()函數第一個參數數組中,因此說AMD是依賴前置的。這不一樣於CMD規範,它是依賴就近的。
CMD推崇依賴就近,延遲執行。能夠把你的依賴寫進代碼的任意一行,以下:
define(factory)
factory
爲函數時,表示是模塊的構造方法。執行該構造方法,能夠獲得模塊向外提供的接口。factory 方法在執行時,默認會傳入三個參數:require、exports 和 module.
// CMD define(function(require, exports, module) { var a = require('./a'); a.doSomething(); var b = require('./b'); b.doSomething(); })
若是使用AMD寫法,以下:
// AMDdefine(['a', 'b'], function(a, b) { a.doSomething(); b.doSomething(); })
這個規範其實是爲了Seajs的推廣而後搞出來的。那麼看看SeaJS是怎麼回事兒吧,基本就是知道這個規範了。
一樣Seajs也是預加載依賴js跟AMD的規範在預加載這一點上是相同的,明顯不一樣的地方是調用,和聲明依賴的地方。AMD和CMD都是用difine和require,可是CMD標準傾向於在使用過程當中提出依賴,就是無論代碼寫到哪忽然發現須要依賴另外一個模塊,那就在當前代碼用require引入就能夠了,規範會幫你搞定預加載,你隨便寫就能夠了。可是AMD標準讓你必須提早在頭部依賴參數部分寫好(沒有寫好? 倒回去寫好咯)。這就是最明顯的區別。
sea.js經過sea.use()
來加載模塊。
seajs.use(id, callback?)
es6模塊特性,推薦參看阮一峯老師的:ECMAScript 6 入門 - Module 的語法
提及 ES6 模塊特性,那麼就先說說 ES6 模塊跟 CommonJS 模塊的不一樣之處。
ES6 模塊輸出的是值的引用,輸出接口動態綁定,而 CommonJS 輸出的是值的拷貝
ES6 模塊編譯時執行,而 CommonJS 模塊老是在運行時加載
CommonJS 模塊輸出的是值的拷貝(原始值的拷貝),也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。
// a.js var b = require('./b'); console.log(b.foo); setTimeout(() => { console.log(b.foo); console.log(require('./b').foo); }, 1000); // b.js let foo = 1; setTimeout(() => { foo = 2; }, 500); module.exports = { foo: foo, }; // 執行:node a.js // 執行結果: // 1 // 1 // 1
上面代碼說明,b 模塊加載之後,它的內部 foo 變化就影響不到輸出的 exports.foo 了。這是由於 foo 是一個原始類型的值,會被緩存。因此若是你想要在 CommonJS 中動態獲取模塊中的值,那麼就須要藉助於函數延時執行的特性。
// a.js var b = require('./b'); console.log(b.foo()); setTimeout(() => { console.log(b.foo()); console.log(require('./b').foo()); }, 1000); // b.js let foo = 1; setTimeout(() => { foo = 2; }, 500); module.exports = { foo: () => { return foo; }, }; // 執行:node a.js // 執行結果: // 1 // 2 // 2
因此咱們能夠總結一下:
CommonJS 模塊重複引入的模塊並不會重複執行,再次獲取模塊直接得到暴露的 module.exports 對象
若是你要到處獲取到模塊內的最新值的話,也能夠你每次更新數據的時候每次都要去更新 module.exports 上的值
若是你暴露的 module.exports 的屬性是個對象,那就不存在這個問題了
因此若是你要到處獲取到模塊內的最新值的話,也能夠你每次更新數據的時候每次都要去更新 module.exports 上的值,好比:
// a.js var b = require('./b'); console.log(b.foo); setTimeout(() => { console.log(b.foo); console.log(require('./b').foo); }, 1000); // b.js module.exports.foo = 1; // 同 exports.foo = 1 setTimeout(() => { module.exports.foo = 2; }, 500); // 執行:node a.js // 執行結果: // 1 // 2 // 2
然而在 ES6 模塊中就再也不是生成輸出對象的拷貝,而是動態關聯模塊中的值。
關於第二點,ES6 模塊編譯時執行會致使有如下兩個特色:
import 命令會被 JavaScript 引擎靜態分析,優先於模塊內的其餘內容執行。
export 命令會有變量聲明提早的效果。
import 優先執行:
從第一條來看,在文件中的任何位置引入 import 模塊都會被提早到文件頂部。
// a.js console.log('a.js') import { foo } from './b'; // b.js export let foo = 1; console.log('b.js 先執行'); // 執行結果: // b.js 先執行 // a.js
從執行結果咱們能夠很直觀地看出,雖然 a 模塊中 import 引入晚於 console.log('a'),可是它被 JS 引擎經過靜態分析,提到模塊執行的最前面,優於模塊中的其餘部分的執行。
因爲 import 是靜態執行,因此 import 具備提高效果即 import 命令在模塊中的位置並不影響程序的輸出。
/ a.js import { foo } from './b'; console.log('a.js'); export const bar = 1; export const bar2 = () => { console.log('bar2'); } export function bar3() { console.log('bar3'); } // b.js export let foo = 1; import * as a from './a'; console.log(a); // 執行結果: // { bar: undefined, bar2: undefined, bar3: [Function: bar3] } // a.js
從上面的例子能夠很直觀地看出,a 模塊引用了 b 模塊,b 模塊也引用了 a 模塊,export 聲明的變量也是優於模塊其它內容的執行的,可是具體對變量賦值須要等到執行到相應代碼的時候。(固然函數聲明和表達式聲明不同,這一點跟 JS 函數性質同樣,這裏就不過多解釋)
好了,講完了 ES6 模塊和 CommonJS 模塊的不一樣點以後,接下來就講講相同點:
模塊不會重複執行
這個很好理解,不管是 ES6 模塊仍是 CommonJS 模塊,當你重複引入某個相同的模塊時,模塊只會執行一次。
CommonJS 模塊循環依賴
// a.js console.log('a starting'); exports.done = false; const b = require('./b'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); exports.done = false; const a = require('./a'); console.log('in b, a.done =', a.done); exports.done = true; console.log('b done'); // node a.js // 執行結果: // a starting // b starting // in b, a.done = false // b done // in a, b.done = true // a done
結合以前講的特性很好理解,當你從 b 中想引入 a 模塊的時候,由於 node 以前已經加載過 a 模塊了,因此它不會再去重複執行 a 模塊,而是直接去生成當前 a 模塊吐出的 module.exports 對象,由於 a 模塊引入 b 模塊先於給 done 從新賦值,因此當前 a 模塊中輸出的 module.exports 中 done 的值仍爲 false。而當 a 模塊中輸出 b 模塊的 done 值的時候 b 模塊已經執行完畢,因此 b 模塊中的 done 值爲 true。
從上面的執行過程當中,咱們能夠看到,在 CommonJS 規範中,當遇到 require() 語句時,會執行 require 模塊中的代碼,並緩存執行的結果,當下次再次加載時不會重複執行,而是直接取緩存的結果。正由於此,出現循環依賴時纔不會出現無限循環調用的狀況。雖然這種模塊加載機制能夠避免出現循環依賴時報錯的狀況,但稍不注意就極可能使得代碼並非像咱們想象的那樣去執行。所以在寫代碼時仍是須要仔細的規劃,以保證循環模塊的依賴能正確工做。
因此有什麼辦法能夠出現循環依賴的時候避免本身出現混亂呢?一種解決方式即是將每一個模塊先寫 exports 語法,再寫 requre 語句,利用 CommonJS 的緩存機制,在 require() 其餘模塊以前先把自身要導出的內容導出,這樣就能保證其餘模塊在使用時能夠取到正確的值。好比:
// a.js exports.done = true; let b = require('./b'); console.log(b.done) // b.js exports.done = true; let a = require('./a'); console.log(a.done)
這種寫法簡單明瞭,缺點是要改變每一個模塊的寫法,並且大部分同窗都習慣了在文件開頭先寫 require 語句。
跟 CommonJS 模塊同樣,ES6 不會再去執行重複加載的模塊,又因爲 ES6 動態輸出綁定的特性,能保證 ES6 在任什麼時候候都能獲取其它模塊當前的最新值。
// a.js console.log('a starting') import {foo} from './b'; console.log('in b, foo:', foo); export const bar = 2; console.log('a done'); // b.js console.log('b starting'); import {bar} from './a'; export const foo = 'foo'; console.log('in a, bar:', bar); setTimeout(() => { console.log('in a, setTimeout bar:', bar); }) console.log('b done'); // babel-node a.js // 執行結果: // b starting // in a, bar: undefined // b done // a starting // in b, foo: foo // a done // in a, setTimeout bar: 2
ES6 模塊在編譯時就會靜態分析,優先於模塊內的其餘內容執行,因此致使了咱們沒法寫出像下面這樣的代碼:
if(some condition) { import a from './a'; }else { import b from './b'; } // or import a from (str + 'b');
由於編譯時靜態分析,致使了咱們沒法在條件語句或者拼接字符串模塊,由於這些都是須要在運行時才能肯定的結果在 ES6 模塊是不被容許的,因此 動態引入 import() 應運而生。
import() 容許你在運行時動態地引入 ES6 模塊,想到這,你可能也想起了 require.ensure 這個語法,可是它們的用途卻大相徑庭的。
require.ensure 的出現是 webpack 的產物,它是由於瀏覽器須要一種異步的機制能夠用來異步加載模塊,從而減小初始的加載文件的體積,因此若是在服務端的話 require.ensure 就無用武之地了,由於服務端不存在異步加載模塊的狀況,模塊同步進行加載就能夠知足使用場景了。 CommonJS 模塊能夠在運行時確認模塊加載。
而 import() 則不一樣,它主要是爲了解決 ES6 模塊沒法在運行時肯定模塊的引用關係,因此須要引入 import()
咱們先來看下它的用法:
動態的 import() 提供一個基於 Promise 的 API
動態的import() 能夠在腳本的任何地方使用
import() 接受字符串文字,你能夠根據你的須要構造說明符
舉個簡單的使用例子:
// a.js const str = './b'; const flag = true; if(flag) { import('./b').then(({foo}) => { console.log(foo); }) } import(str).then(({foo}) => { console.log(foo); }) // b.js export const foo = 'foo'; // babel-node a.js // 執行結果 // foo // foo
固然,若是在瀏覽器端的 import() 的用途就會變得更普遍,好比 按需異步加載模塊,那麼就和 require.ensure 功能相似了。
由於是基於 Promise 的,因此若是你想要同時加載多個模塊的話,能夠是 Promise.all 進行並行異步加載。
Promise.all([ import('./a.js'), import('./b.js'), import('./c.js'), ]).then(([a, {default: b}, {c}]) => { console.log('a.js is loaded dynamically'); console.log('b.js is loaded dynamically'); console.log('c.js is loaded dynamically'); });
還有 Promise.race 方法,它檢查哪一個 Promise 被首先 resolved 或 reject。咱們可使用import()來檢查哪一個CDN速度更快:
const CDNs = [ { name: 'jQuery.com', url: 'https://code.jquery.com/jquery-3.1.1.min.js' }, { name: 'googleapis.com', url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js' } ]; console.log(`------`); console.log(`jQuery is: ${window.jQuery}`); Promise.race([ import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')), import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded')) ]).then(()=> { console.log(`jQuery version: ${window.jQuery.fn.jquery}`); });
固然,若是你以爲這樣寫還不夠優雅,也能夠結合 async/await 語法糖來使用。
async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]); }
動態 import() 爲咱們提供了以異步方式使用 ES 模塊的額外功能。 根據咱們的需求動態或有條件地加載它們,這使咱們可以更快,更好地建立更多優點應用程序。
一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是但願外部文件可以讀取該模塊的變量,就須要在這個模塊內使用export關鍵字導出變量。如:
// profile.jsexport var a = 1;export var b = 2;export var c = 3;1234
下面的寫法是等價的,這種方式更加清晰(在底部一眼能看出導出了哪些變量):
var a = 1;var b = 2;var c = 3; export {a, b, c}1234
import命令能夠導入其餘模塊經過export導出的部分。
var a = 1;var b = 2;var c = 3; export {a, b, c} //main.js import {a, b, c} from './abc'; console.log(a, b, c);
若是想爲導入的變量從新取一個名字,使用as關鍵字(也能夠在導出中使用)。
import {a as aa, b, c}; console.log(aa, b, c)12
若是想在一個模塊中先輸入後輸出一個模塊,import語句能夠和export語句寫在一塊兒。
import {a, b, c} form './abc';export {a, b, c}// 使用連寫, 可讀性很差,不建議export {a, b, c} from './abc';12345
使用*關鍵字。
import * from as abc form './abc';
在export輸出內容時,若是同時輸出多個變量,須要使用大括號{}
,同時導入也須要大括號。使用export defalut
輸出時,不須要大括號,而輸入(import)export default
輸出的變量時,不須要大括號。
// abc.jsvar a = 1, b = 2, c = 3;export {a, b};export default c;1234
import {a, b} from './abc'; import c from './abc'; // 不須要大括號console.log(a, b, c) // 1 2 3123
本質上,export default
輸出的是一個叫作default的變量或方法,輸入這個default變量時不須要大括號。
// abc.js export {a as default}; // main.js import a from './abc'; // 這樣也是能夠的 import {default as aa} from './abc'; // 這樣也是能夠的 console.log(aa);123456789
就到這裏了吧。關於循環加載(模塊相互依賴)沒寫,CommonJS和ES6處理方式不同。
參考文章: