一篇文章幫你弄懂ES6模塊化

前端模塊化是前端工程化的基石。時下,大前端時代中對模塊的運用更是無處不在,而放眼將來,es6中所提出的import和export的形式彷佛將統一先後端的模塊化加載。

模塊化概述

在學習ES6的模塊化以前先複習一下以前出現的模塊化,比較經常使用的有三種規範定義:CommonJS、AMD、CMD。前端

它們的特色與相互間的區別是:es6

  1. CommonJS適用於服務端,寫法爲:
var clock = require('clock.js')
clock.start();

上例表示,clock的調用必須等待clock.js請求加載成功,換句話說,是同步操做,而這也致使了CommonJS普遍應用於服務端而不是客戶端(服務器讀取模塊都是在本地磁盤,加載速度快,而若是是在客戶端則容易出現‘假死’狀態)那麼能不能用異步加載模塊呢?express

  1. AMD (Asynchronous Module Definition) 就是異步加載模塊,多用於瀏覽器requireJs應用了這一規範),寫法爲:
require([module],callback);

// eg
require(['clock.js'],function(clock){
  clock.start();
})

雖然實現了異步加載,規避了瀏覽器的「假死」問題,可是也存在缺點: 一開始就把全部依賴寫出來是不符合邏輯順序的。那麼,能不能像CommonJS同樣用的時候才require,而後還能支持異步加載後執行呢後端

  1. CMD (Common Module Definition) 則是依賴就近,用的時候再requireseajs推崇的規範 ),寫法爲:
define(function(require,exports,module){
  var clock = require('clock.js');
  clock.start();
})

AMD和CMD的區別是對依賴模塊的執行時機不一樣,而不是加載處理方式不一樣,兩者皆爲異步加載模塊。 前端工程化

AMD依賴前置js能夠方便地清楚依賴模塊有哪些,當即加載;瀏覽器

CMD就近依賴,開發者能夠在須要用到依賴的時候再require,可是對於js處理器來講,須要把代碼處理爲字符串解析一遍才知道依賴了哪些模塊,即犧牲性能來得到開發的便利,雖然實際上解析的時間短到能夠忽略,可是也有不少人詬病CMD這一點。緩存

ES6的模塊化設計思想是儘可能靜態化,使得編譯時就能肯定模塊的依賴關係。服務器

對比CommonJS和ES6模塊:app

// CommonJS
let { start, exists, readFile } = require('fs')
// 至關於
// let _fs = require('fs')
// let start = _fs.start, exists = _fs.exists, readFile = _fs.readFile

// ES6
import { start, exists, readFile } from 'fs'

上述例子中,CommonJS的實質是總體加載fs模塊生成一個_fs對象,以後再從對象中分別讀取3個方法,稱爲「運行時加載」。而ES6模塊是加載3個方法,稱爲「編譯時加載dom

ES6模塊化的語法規範

嚴格模式

在ES6模塊中自動採用嚴格模式。規定:

  • 變量必須先聲明
  • 函數參數不能有同名屬性
  • 不能使用with
  • 對只讀屬性賦值、delete不可刪除屬性直接報錯
  • 不可刪除變量delete prop、只能刪除屬性delete global[prop]
  • eval不會再外層做用域引入變量
  • evalarguments不可從新賦值
  • arguments不會自動反應函數參數變化
  • 禁止this指向全局
  • 增長保留字:static、interface、protected等。
注意:在ES6模塊中,頂層 thisundefined,不該該被使用。

export命令

第一種:

export var a = '123';
export const _b = '2323'
export let c = '2222'

第二種:

var a = '123';
const _b = '2323'
let c = '2222' 
export {a, _b, c}; // 推薦

第三種(第二種基礎上加上as關鍵詞重命名)

var a = '123';
const _b = '2323'
let c = '2222' 
export {
    a as stream1,
  _b as stream2,
  c as stream3};

注意:

  1. export語句輸出的接口是對應值的引用,也就是一種動態綁定關係,經過該接口能夠獲取模塊內部實時的值。

    對比CommonJS規範:CommonJS模塊輸出的是值的緩存,不存在動態更新。

  2. export命令規定要處於模塊頂層,不過出如今塊級做用域內,就會報錯,import同理

import命令

第一種:

import {a, _b ,c} from './profile'

第二種:

import {stream1 as firstVal} from './profile'

import 是靜態執行,不能夠應用表達式、變量和if結構。

if(x == 1){
  import { foo } from 'module1'
}else{
  //...
}

import語句是Singleton模式:雖然foobar在兩個語句中加載,可是對應的是同一個my_module實例。

import { foo } from './module1'
import { bar } from './module1'

// 至關於
import {foo,bar} from './module1'

模塊的總體加載

可使用*來指定一個對象,全部輸出值都加載到這個對象上:

import * as circle from './module1'
circle.foo();
circle.bar();

因爲模塊總體加載所在的對象都是能夠靜態分析的,因此不容許運行時改變。

import * as circle from './module1'
// 下面兩行都是不容許的
circle.foo = 123;
circle.bar = function(){}

默認輸出

export default命令能夠爲模塊默認輸出

// module2.js
export default function(){
  console.log('123')
}
// 至關於
function a(){
  console.log('123')
}
export {a as default};

import命令能夠爲匿名函數指定任意名字

import defaultFn from './module2'
// 至關於
import {default as defaultFn} from './module2'

export和import的複合寫法

export { foo, bar} from 'my_module';
// 等同於
import {foo,bar} from 'my_module';
export{foo,bar};
export {es6 as default} from './someModule'
// 等同於
import {es6} from './someModule'
export default es6;

import()方法

前面提到過,require是動態加載,便可以在用的時候再require;而import是靜態執行,只能處於代碼最頂層,不能夠存在於塊級做用域中。這致使import沒法在運行中執行(相似於AMD的缺點)。
因而就有了一種提案:引入import()函數,相似於Node的require函數(CommonJS),可是它實現了異步加載。

定義:import()函數接收與import相同的參數,返回一個Promise對象,加載獲取到的值做爲then方法的回調參數。

const main = document.querySelector('main')

import(`./section-modules/${someVariable}.js`)
    .then(module => {
      module.loadPageInto(main);
    })
    .catch(err => {
    main.textContext = err.message;
  })
// 加載得到接口參數:
import('./module1.js')
.then(({default:defaultFn,foo,bar}) => {
  console.log(defaultFn)
})
// 同時加載多個模塊並應用於async函數中
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')
        ])
}
main();

不一樣規範間加載

import加載CommonJS模塊

  • 使用import命令加載CommonJS模塊,Node會自動將module.exports屬性當作模塊的默認輸出,即等同於export default

    // a.js
    module.exports = {
      foo: 'hello',
      bar: 'world'
    }
    
    // 在import引入時等同於
    export default {
      foo: 'hello',
      bar: 'world'
    }
  • CommonJs模塊是運行時肯定輸出接口,因此採用import命令加載CommonJS模塊時,只能使用總體輸入(*)。

    import {readfile} from 'fs' //當'fs'爲CommonJS模塊時錯誤
    // 總體輸入
    import * as express from 'express'
    const app = express.default();

require加載ES6模塊

  • require命令加載ES6模塊時,全部的輸出接口都會成爲輸入對象的屬性。

    // es.js
    let foo = {bar : 'my-default'};
    exxport default foo;
    foo = null;
    
    // cjs.js
    const es_namespace = require('./es')
    console.log(es_namespace.default);// {bar:'my-default'}

對比CommonJS

有了新歡也不能忘了舊愛,讓咱們再來繼續對比CommonJS和ES6模塊化的區別,進一步體會理解ES6模塊化的特性。

輸出差別

CommonJS模塊輸出的是一個值的複製,ES6輸出的是值的引用

// lib.js 
let num = 3;
function changeNum() {
  num = 4;
}
module.exports = {
  num: num,
  changeNum: changeNum,
};

//main.js
var mod = require('./lib.js')
console.log(mod.num); // 3
mod.changeNum();
console.log(mod.num); // 3

這是因爲,mod.num是一個原始類型的值,會被緩存。能夠經過寫成一個函數,來獲得內部修改後的值:

// lib.js 
let num = 3;
function changeNum() {
  num = 4;
}
module.exports = {
  get num(){
    return num
  },
  changeNum: changeNum,
};

//main.js
var mod = require('./lib.js')
console.log(mod.num); // 3
mod.changeNum();
console.log(mod.num); // 3

對比ES6模塊:

// lib.js 
export let num = 3;
export function changeNum() {
  num = 4;
}

//main.js
import {num,changeNum} from './lib.js'
console.log(num); // 3
changeNum();
console.log(num); // 4

CommonJS的循環加載

加載原理

CommonJS一個模塊對應一個腳本文件,require命令每次加載一個模塊就會執行整個腳本,而後生成一個對象。這個對象一旦生成,之後再次執行相同的require命令都會直接到緩存中取值。也就是說:CommonJS模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載時就返回第一次運行的結果,除非手動清除系統緩存。

循環加載

// a.js
exports.done = false;
var b = require('./b.js'); // 1. a.js暫停執行,轉到執行b.js ; b.js完畢後回來,b:{done:true}
console.log('在a.js中,b.done=%j',b.done); // 5. '在a.js中,b.done=true'
exports.done = true;
console.log('a.js執行完畢') // 6. 'a.js執行完畢'

// b.js
exports.done = false;
var a = require('./b.js') // 2. a:{done:false}
console.log('在b.js中,a.done=%j',a.done); // 3. '在b.js中,a.done=false'
exports.done = true;
console.log('b.js執行完畢') // 4. 'b.js執行完畢',繼續執行a.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); // 7.'在main.js中,a.done=true,b.done=true'

上面代碼能夠看到:第一,在b.js中,a.js沒有執行完畢,第二,當main.js執行到第二行時不會再次執行b.js,而是輸出緩存的b.js的執行結果,即它的第四行:exports.done = true

總結一下:

  1. 因爲CommonJS模塊遇到循環加載返回的是當前已經執行的部分的值,而不是代碼所有執行後的值(上面的第2步註釋)
  2. CommonJS輸入的是被輸出值的緩存,而非動態引用。

對比:ES6模塊是動態引用,變量不會被緩存

// a.js
import {bar} from './b.js';
export function foo(){
  console.log('foo')
  bar();
  console.log('執行完畢')
}
foo();

// b.js
import {foo} from './a.js' // 若是爲CommonJS,這裏直接就返回undefined值且不會再更改
export function bar(){
  console.log('bar')
  if(Math.random() > 0.5){
    foo();
  }
}

// 執行結果可能爲:foo bar 執行完畢
// 執行結果也可能爲: foo bar foo bar 執行完畢 執行完畢
相關文章
相關標籤/搜索