歷史上,js沒有模塊化的概念,不能把一個大工程分解成不少小模塊。這對於多人開發大型,複雜的項目造成了巨大的障礙,明顯下降了開發效率,java,Python有import,甚至連css都有@import,可是使人費解的是js竟然沒有這方面的支持。es6出現以後才解決了這個問題,在這以前,各大社區也都出現了不少解決方法,比較出色的被你們廣爲流傳的就有AMD,CMD,commonjs,UMD,今天咱們就來分析這幾個模塊化的解決方案。javascript
上面提到的幾種模塊化的方案的模塊加載有何異同呢?
先來講下es6模塊,es6模塊的設計思想是儘可能靜態化,使得編譯時就能肯定依賴關係,被稱爲編譯時加載。其他的都只能在運行時肯定依賴關係,這種被稱爲運行時加載。下面來看下例子就明白了,好比下面這段代碼css
let {a,b,c} = require("util");//會加載util裏的全部方法,使用時只用到3個方法 import {a,b,c} from 'util';//從util中加載3個方法,其他不加載
下面簡單介紹一下AMD,CMD,commonjs,UMD這幾種模塊化方案。前端
commonjs是服務端模塊化採用的規範,nodejs就採用了這個規範。
根據commonjs的規範,一個單獨的文件就是一個模塊,加載模塊使用require方法,該方法讀取文件並執行,返回export對象。java
// foobar.js //私有變量 var test = 123; //公有方法 function foobar () { this.foo = function () { // do someing ... } this.bar = function () { //do someing ... } } //exports對象上的方法和變量是公有的 var foobar = new foobar(); exports.foobar = foobar; //讀取 var test = require('./foobar').foobar; test.bar();
CommonJS 加載模塊是同步的,因此只有加載完成才能執行後面的操做。像Node.js主要用於服務器的編程,加載的模塊文件通常都已經存在本地硬盤,因此加載起來比較快,不用考慮異步加載的方式,因此CommonJS規範比較適用。但若是是瀏覽器環境,要從服務器加載模塊,這是就必須採用異步模式。因此就有了 AMD CMD 解決方案。node
AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"
AMD設計出一個簡潔的寫模塊API:es6
define(id?, dependencies?, factory);
第一個參數 id 爲字符串類型,表示了模塊標識,爲可選參數。若不存在則模塊標識應該默認定義爲在加載器中被請求腳本的標識。若是存在,那麼模塊標識必須爲頂層的或者一個絕對的標識。
第二個參數,dependencies ,是一個當前模塊依賴的,已被模塊定義的模塊標識的數組字面量。
第三個參數,factory,是一個須要進行實例化的函數或者一個對象。編程
看下下面的例子就明白了數組
define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){ export.verb = function(){ return beta.verb(); // or: return require("beta").verb(); } });
提到AMD就不得不提requirejs。
RequireJS 是一個前端的模塊化管理的工具庫,遵循AMD規範,它的做者就是AMD規範的創始人 James Burke。
AMD的基本思想就是先加載須要的模塊,而後返回一個新的函數,全部的操做都在這個函數內部操做,以前加載的模塊在這個函數裏是能夠調用的。瀏覽器
CMD是seajs在推廣的過程當中對模塊的定義的規範化產出
和AMD提早執行不一樣的是,CMD是延遲執行,不過requirejs從2.0開始也開始支持延遲執行了,這取決於寫法。
AMD推薦的是依賴前置,CMD推薦的是依賴就近。
看下AMD和CMD的代碼緩存
//AMD define(['./a','./b'], function (a, b) { //依賴一開始就寫好 a.test(); b.test(); }); //CMD define(function (requie, exports, module) { //依賴能夠就近書寫 var a = require('./a'); a.test(); ... //軟依賴 if (status) { var b = requie('./b'); b.test(); } });
UMD是AMD和commonjs的結合
AMD適用瀏覽器,commonjs適用服務端,若是結合了二者就達到了跨平臺的解決方案。
UMD先判斷是否支持AMD(define是否存在),存在用AMD模塊的方式加載模塊,再判斷是否支持nodejs的模塊(exports是否存在),存在用nodejs模塊的方式,不然掛在window上,當全局變量使用。
這也是目前不少插件頭部的寫法,就是用來兼容各類不一樣模塊化的寫法。
(function(window, factory) { //amd if (typeof define === 'function' && define.amd) { define(factory); } else if (typeof exports === 'object') { //umd module.exports = factory(); } else { window.jeDate = factory(); } })(this, function() { ...module..code... })
es6的模塊自動採用嚴格模式,無論有沒有在頭部加上'use strict'
模塊是由export和import兩個命令構成。
//a.js export default function(){ console.log('aaa'); } //b.js import aaa from 'a.js';
1.使用export default的時候,對應的import不須要使用大括號,import命令能夠爲default指定任意的名字。
2.不適用export default的時候,對應的import是須要使用大括號的
3.一個export default只能使用一次
import 'lodash;
上面的代碼僅僅執行了lodash模塊,沒有輸入任何值
總體加載有兩種方式
//import import * as circle from './circle' //module //module後面跟一個變量,表示輸入的模塊定義在該變量上 module circle from './circle'
在講循環加載前,先了解下commonjs和es6模塊加載的原理
commonjs的一個模塊就是一個腳本文件,require命令第一次加載腳本的時候就會執行整個腳本,而後在內存中生成一個對象
{ id:"...", exports: {...}, loaded: true, ... }
上面的對象中,id是模塊名,exports是模塊輸出的各個接口,loaded是一個布爾值,表示該模塊的腳本是否執行完畢.
以後要用到這個模塊時,就會到exports上取值,即便再次執行require命令,也不會執行該模塊,而是到緩存中取值
commonjs模塊輸入的是被輸出值的拷貝,也就是說一旦輸出一個值,模塊內部的變化就影響不到這個值
es6的運行機制和commonjs不同,它遇到模塊加載命令import不會去執行模塊,只會生成一個動態的只讀引用,等到真正要用的時候,再到模塊中去取值,因爲es6輸入的模塊變量只是一個‘符號連接’,因此這個變量是隻讀的,對他進行從新賦值會報錯。
import {obj} from 'a.js'; obj.a = 'qqq';//ok obj = {}//typeError
分析完二者的加載原理,來看下二者的循環加載
commonjs模塊的重要特性是加載時執行,即代碼在require的時候就會執行,commonjs的作法是一旦出現循環加載,就只輸出已經執行的部分,還未執行的部分不會輸出.
下面來看下commonjs中的循環加載的代碼
//a.js exports.done = false; var b = require('./b.js'); console.log('在a.js中,b.done=',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=',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=',a.done,',b.done=',b.done);
上面的代碼中,執行a.js的時候,a.js先輸出done變量,而後加載另外一個腳本b.js,此時a的代碼就停在這裏,等待b.js執行完畢,再往下執行。而後看下b.js的代碼,b.js也是先輸出done變量,而後加載a.js,這時發生了循環加載,按照commonjs的機制,系統會去a.js中的exports上取值,但是其實a.js是沒有執行完的,只能輸出已經執行的部分done=false,而後b.js繼續執行,執行完畢後將執行權返回給a.js,因而a.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
上面這個例子說了兩點
2.main.js中執行到第二行不會再次執行b.js,而是輸出緩存的b.js的執行結果,即第4行
es6處理循環加載和commonjs不一樣,es6是動態引用,遇到模塊加載命令import時不會去執行模塊,只會生成一個指向模塊的引用,須要開發者本身保證能取到輸出的值
看下面的例子
//a.js import {odd} from 'b.js'; export counter = 0; export function even(n){ counter++; return n==0 || odd(n-1); } //b.js import {even} from 'a.js'; export function odd(n){ return n!=0 && even(n-1); } //main.js import {event,counter } from './a.js'; event(10) counter //6
執行main.js,按照commonjs的規範,上面的代碼是沒法執行的,由於a先加載b,b又加載a,可是a又沒有輸出值,b的even(n-1)會報錯可是es6能夠執行,結果是6