require()寞埠、import、import()加載模塊詳解(二)

接上篇 require()焊夸、import、import()加載模塊詳解(一)

ES6 Module 的 import

通過 import 靜態(tài)地導(dǎo)入另一個(gè)通過 export 導(dǎo)出的模塊蓝角。

區(qū)分于 CJS 運(yùn)行時(shí)才和導(dǎo)入模塊建立關(guān)系阱穗,ESM 在轉(zhuǎn)化成中間代碼時(shí)(編譯階段) import 語句就和模塊建立了靜態(tài)引用關(guān)系,在運(yùn)行時(shí)導(dǎo)入和導(dǎo)出是不可更改的使鹅。這就意味著我們只能在頂層進(jìn)行導(dǎo)入和導(dǎo)出 (比如絕不能嵌套在條件語句中)揪阶,同時(shí) import 和 export 語句不能有「具有邏輯或含有變量」的動態(tài)部分,即不能依賴于運(yùn)行時(shí)計(jì)算的任何內(nèi)容 (如import foo from './module' + 變量;)患朱,不然編譯時(shí)就會報(bào)錯(cuò)鲁僚。而 require 可以在運(yùn)行時(shí)通過 if 判斷決定導(dǎo)入哪個(gè)模塊。

在編譯期裁厅,import 語句會被內(nèi)部移動至當(dāng)前作用域最開頭 (類似 var 和 function 的變量提升)冰沙,先于其他代碼執(zhí)行。JS 解析器編譯到 import 語句時(shí)执虹,會生成一個(gè)接口標(biāo)識符或默認(rèn)導(dǎo)出接口對應(yīng)的引用拓挥。如 import { a } from './module-a',a 指向的是export const a = xxx 接口中的 a袋励;而 import defaultB from './module-b'侥啤,defaultB 指向的是 export default b 中的 b (默認(rèn)接口導(dǎo)入時(shí)的名稱可以自定義)当叭。到了運(yùn)行期,也不會去執(zhí)行完整模塊盖灸,只有在調(diào)用 a / defaultB 的時(shí)候才會加載模塊中相應(yīng)的接口取值蚁鳖。

換句話說,ESM模塊規(guī)則有點(diǎn)像Unix系統(tǒng)的“符號連接”赁炎,原始值變了醉箕,import 輸入的值也會跟著變。導(dǎo)入的變量綁定其所在的模塊甘邀,不會緩存值琅攘。不同腳本加載同一個(gè)模塊得到的是同一個(gè)實(shí)例。因此ESM設(shè)定了不能修改導(dǎo)入值的只讀規(guī)則松邪。

CJS 導(dǎo)入的是導(dǎo)出值的淺拷貝副本坞琴,而ESM導(dǎo)入是導(dǎo)出值的實(shí)時(shí)只讀引用。

靜態(tài)型的 import 是初始化加載依賴項(xiàng)的最優(yōu)選擇逗抑, 靜態(tài)模塊結(jié)構(gòu) 更容易從代碼靜態(tài)分析工具和 tree shaking 中受益剧辐。而且自動支持模塊間的循環(huán)依賴。
在用 webpack邮府、Rollup 這樣的模塊打包器時(shí)荧关,證明ESM模塊可以更高效地組合:

  • 加載所有模塊時(shí),import 查找變量是靜態(tài)檢索褂傀,比 require() 的動態(tài)檢索快很多忍啤。
  • 壓縮綁定的文件比壓縮單獨(dú)的文件效率更高。
  • 在綁定過程中仙辟,通過刪除未使用的出口代碼同波,從而節(jié)省大量空間。

在瀏覽器中叠国,import 語句只能在 <script type="module"></script> 標(biāo)簽中使用 (<script type="module"> 擁有自己的局部作用域)未檩。或者寫在.mjs擴(kuò)展名的文件里粟焊。

語法:

ESM模塊有兩種導(dǎo)出方式:命名導(dǎo)出(每個(gè)模塊可以幾個(gè))和默認(rèn)導(dǎo)出(每個(gè)模塊一個(gè))冤狡。可以同時(shí)使用兩者项棠,但通常最好將它們分開悲雳。

命名導(dǎo)出:export
// 1. 關(guān)鍵字標(biāo)記聲明
// 導(dǎo)出單個(gè)聲明常量/變量
export const name1 = … // 用 let, var 定義變量也可,不過通常還是常量
export let name2 = …
// 導(dǎo)出聲明函數(shù)
export function functionName() {...}
// 導(dǎo)出聲明類
export class className {...}

// 2. 用對象列出要導(dǎo)出的所有內(nèi)容
// name1沾乘,name2... 是事先定義好的標(biāo)識符怜奖。如果在一個(gè)模塊要導(dǎo)出多個(gè)值,同時(shí)數(shù)量不算多時(shí)推薦這樣做翅阵,代碼結(jié)構(gòu)會比較清晰
const name1 = …
const name2 = …
export { name1, name2, …, nameN }
// 重命名導(dǎo)出
export { variable1 as name1, variable2 as name2, …, nameN }
  • name1… nameN歪玲、functionName迁央、className —— 要導(dǎo)出的“標(biāo)識符”。在其他腳本 import 時(shí)需要用這些“標(biāo)識符”進(jìn)行針對性的導(dǎo)入

直接在 export 關(guān)鍵字后面聲明的語句叫 內(nèi)聯(lián)導(dǎo)出

export const name1 = 11
export function foo() {}
// 等效于
const name1 = 11
function foo() {}
export { name1, foo };

同時(shí)不能直接 export 一個(gè)對象滥崩,如export { name1: 1, name2: 2 }岖圈,export { ... }只允許放用,分隔的標(biāo)識符。因?yàn)椴荒芡ㄟ^對象強(qiáng)制執(zhí)行靜態(tài)關(guān)聯(lián)钙皮,從而失去所有靜態(tài)模塊結(jié)構(gòu)相關(guān)的優(yōu)勢蜂科。

默認(rèn)導(dǎo)出:export default

實(shí)質(zhì)上是個(gè)語法糖。export default 命令就是將輸出內(nèi)容賦值給名為 default 的 變量短条,導(dǎo)出內(nèi)容可以是任意表達(dá)式 (函數(shù)或Class也在內(nèi))导匣,在導(dǎo)入時(shí)可以隨意為這個(gè) default 更名。因?yàn)橐呀?jīng)聲明變量 default 了茸时,后面就不能跟變量聲明語句了贡定,這一點(diǎn)要和 export 區(qū)分開。

expression(表達(dá)式) 屬于 satement(語句)可都,但 expression 是可以通過 evaluation 產(chǎn)生結(jié)果的缓待。也就是說這個(gè)結(jié)果不是馬上產(chǎn)生,而是需要時(shí)才會被evaluated渠牲。
簡單判斷:可以被當(dāng)作參數(shù)傳遞的就是expression旋炒,一般是放在小括號里的(expression),而 statement 一般是放在大括號里的{ statement }签杈。expression 被放到函數(shù)體內(nèi)就變成了 satement瘫镇。

// 導(dǎo)出
// a.js 
export default ?expression?;
// 等效于
const a = ?expression?;
export { a as default };

// 導(dǎo)入時(shí):
import b from './a.js'
// 等效于
import { default as b } from './a';

默認(rèn)導(dǎo)出的本意是讓 import 時(shí)不受限于接口名稱任意命名模塊,通常用于整個(gè)模塊的導(dǎo)出答姥,如 React 組件汇四。Vue組件則是把組件的數(shù)據(jù)和邏輯以一個(gè)對象的形式導(dǎo)出。默認(rèn)導(dǎo)出簡單類型的常量意義不大踢涌,幾乎不用。命名導(dǎo)出和默認(rèn)導(dǎo)出混用也存在序宦,比如一個(gè)庫是單個(gè)函數(shù)睁壁,但通過該函數(shù)的屬性提供了其他服務(wù):import _, { each } from 'underscore';
為了快速區(qū)分不同模塊互捌,以及導(dǎo)入時(shí)命名的統(tǒng)一潘明,默認(rèn)導(dǎo)出類和函數(shù)的時(shí)候還是建議命名 (盡管可以匿名)。
同時(shí)一個(gè)js只能有一個(gè) export default秕噪,多個(gè)并存只有最后一個(gè)生效钳降。以下為演示故沒有將多個(gè)注釋掉。

個(gè)人推薦的方式有以下幾種:

// 導(dǎo)出函數(shù)
export default function fun() {} 
// 如果是箭頭函數(shù)腌巾,我寫 React 組件都這樣用
const funArrow = () => {}
export default funArrow

// 導(dǎo)出類
export default class Dog {}

// 導(dǎo)出對象
const foo = 'foo1'
const bar = 'bar2'
export default { foo, bar } // 實(shí)際導(dǎo)出的是 { foo: foo, bar: bar }
// 這里的 foo 和 bar 不是 標(biāo)識符遂填,只是鍵值對同名的簡寫铲觉,有本質(zhì)區(qū)別, 注意區(qū)分

// 也可以直接將值寫在對象里吓坚,Vue組件的做法
export default {
  name: 'foo',
  data: {...}
}
導(dǎo)入 import 類型:

默認(rèn)導(dǎo)入:對應(yīng)默認(rèn)導(dǎo)出撵幽,導(dǎo)入名可以自定義

import customName from 'src/my_lib';

// src/my_lib.js
export default anyThing // 任意類型,函數(shù)礁击、類盐杂、對象 及表達(dá)式

命名空間導(dǎo)入通過 * 導(dǎo)入完整的模塊,把模塊中的全部屬性和方法放到一個(gè)對象中 (每個(gè)命名導(dǎo)出為一個(gè)屬性) 進(jìn)行導(dǎo)入。

import * as my_lib from 'src/my_lib';
console.log(my_lib) // { a, fun }
console.log(my_lib.a) // 'aaa'
my.lib.fun()

// src/my_lib.js
export const a = 'aaa'
export function fun() { ... }

命名導(dǎo)入,可以通過 as 重命名導(dǎo)出標(biāo)識符:

import { name1, name2 as fun } from 'src/my_lib';
console.log(name1)
fun()

// src/my_lib.js
export const name1 = 'aaa'
export function name2() { ... }

空導(dǎo)入:僅加載模塊申窘,不導(dǎo)入任何內(nèi)容敞临。程序中的第一個(gè)此類導(dǎo)入將執(zhí)行模塊的主體。

import 'src/my_lib';

組合導(dǎo)入:導(dǎo)入順序是固定的坟奥,默認(rèn)導(dǎo)出必須始終在第一個(gè)。

// 將默認(rèn)導(dǎo)入與名稱空間導(dǎo)入相結(jié)合:
import theDefault, * as my_lib from 'src/my_lib';
// 將默認(rèn)導(dǎo)入與命名導(dǎo)入結(jié)合
import theDefault, { name1, name2 } from 'src/my_lib';
  • as —— 重命名導(dǎo)出“標(biāo)識符”。比如需要同時(shí)導(dǎo)入兩個(gè)同名的 export 接口食侮,用 as 重命名其中一個(gè)就可以解決沖突
  • from 后面的字符串是要導(dǎo)入的模塊。通常是包含目標(biāo)模塊的.js文件的相對或絕對路徑目胡。
每次 import 都是到導(dǎo)出數(shù)據(jù)的實(shí)時(shí)連接锯七。
//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}
//------ main1.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// The imported value can’t be changed
counter++; // TypeError

如果通過*導(dǎo)入模塊對象,會得到相同的結(jié)果:

//------ main2.js ------
import * as lib from './lib';

// The imported value `counter` is live
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 4

// The imported value can’t be changed
lib.counter++; // TypeError

請注意誉己,雖然不允許直接更改導(dǎo)入的值 (即重新賦值)眉尸,但是可以修改它們引用的對象。例如:

//------ lib.js ------
export let obj = {};

//------ main.js ------
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

ES6 Module 的 import()

靜態(tài) import 命令會被JS引擎靜態(tài)分析巨双,先于其他代碼執(zhí)行噪猾,做不到運(yùn)行時(shí)加載。而且 import 和 export 語句都必須始終位于模塊的頂層筑累,無法按需執(zhí)行袱蜡。為了實(shí)現(xiàn)類似于require的動態(tài)加載,從而提高首屏加載速度慢宗,就出現(xiàn)了一個(gè)import()函數(shù)方法坪蚁。import()括號內(nèi)接收的參數(shù)和import語句from后面的一致。
按照一定的條件或者按需加載模塊的時(shí)候镜沽,動態(tài)import() 是非常有用的敏晤。

import()函數(shù)是動態(tài)按需加載,它返回一個(gè) Promise 對象缅茉。import()是運(yùn)行時(shí)執(zhí)行嘴脾,什么時(shí)候運(yùn)行到這一句,才會加載指定的模塊蔬墩。因此通過 if 判斷可以實(shí)現(xiàn)按條件import()模塊 译打。除了模塊耗拓,還可以用來加載非模塊的腳本。

import()與所加載的模塊沒有靜態(tài)連接關(guān)系扶平,這點(diǎn)也與 import 語句不同 (import語句會建立靜態(tài)引用)帆离。import()類似于 Node 的require(),但區(qū)別是import()為異步加載结澄,而require()是同步加載哥谷。

當(dāng)出現(xiàn)以下的情況,一般就可以用動態(tài)import()代替靜態(tài) import 了:

  • 靜態(tài)導(dǎo)入的模塊很明顯地降低了代碼的加載速度/占用了大量系統(tǒng)內(nèi)存并且被使用的可能性很低麻献,或者并不需要馬上使用它们妥。
  • 被導(dǎo)入的模塊在加載時(shí)還不存在,需要異步獲取
  • 導(dǎo)入模塊的標(biāo)識符需要?jiǎng)討B(tài)構(gòu)建勉吻。(靜態(tài)導(dǎo)入只能使用靜態(tài)標(biāo)識符)
  • 被導(dǎo)入的模塊有副作用(這個(gè)副作用监婶,可以理解為模塊中會直接運(yùn)行的代碼),這些副作用只有在觸發(fā)了某些條件才被需要時(shí)齿桃。

另外請只在必要情況下采用動態(tài)導(dǎo)入惑惶。靜態(tài)框架能更好地初始化依賴,而且更有利于靜態(tài)分析工具和tree shaking發(fā)揮作用短纵。

import('./modules/my-module.js')
  .then(module => {
    // Do something with the module.
  });

因?yàn)槭且粋€(gè) promise带污,import() 也支持 await 關(guān)鍵字。

let module = await import('./modules/my-module.js');
獲取模塊接口

import()加載模塊成功以后香到,這個(gè)模塊會作為一個(gè)對象鱼冀,當(dāng)作then方法的參數(shù)。因此悠就,可以使用對象解構(gòu)賦值的語法千绪,獲取輸出的命名接口

import('./modules/my-module.js')
.then(({export1, export2}) => {
  // ...
});

如上梗脾,export1export2都是my-module.jsexport導(dǎo)出的輸出具名接口荸型,可以直接解構(gòu)獲得。

如果要獲取 default 默認(rèn)導(dǎo)出炸茧,需要用default屬性獲确薄:

import('./modules/my-module.js')
.then(module => {
  console.log(module.default)
});
// 或者這樣
import('./modules/my-module.js')
.then(({default: theDefault}) => {
  console.log(theDefault);
});

總結(jié)

CJS 的 require() 和 exports
  • require() 為同步導(dǎo)入。
  • 動態(tài)結(jié)構(gòu):導(dǎo)入和導(dǎo)出的對象可以在運(yùn)行時(shí)通過變量動態(tài)生成宇立,也可以把 require()/exports 放在 if 語句之類的代碼塊內(nèi)實(shí)現(xiàn)按需加載/導(dǎo)出。
  • 代碼執(zhí)行到 require() 會先把()內(nèi)的模塊代碼執(zhí)行一遍自赔,返回值是模塊導(dǎo)出對象的淺拷貝副本妈嘹。
  • require()進(jìn)來的屬性副本,可以修改和刪除绍妨,簡單類型不會影響被導(dǎo)入模塊润脸,引用類型會改變導(dǎo)入模塊數(shù)據(jù)柬脸。但 require() 的目的主要是導(dǎo)入一些供使用的函數(shù)或常量,這樣顯然是不合理的毙驯,因此盡量不要試圖修改模塊源數(shù)據(jù)倒堕,并在導(dǎo)入時(shí)表明引入的是常量,如:const path = require('path')
  • 需要用 exports.屬性 導(dǎo)出并仔細(xì)地規(guī)劃, 才能使模塊循環(huán)依賴正常工作
ESM 的 import 和 export
  • import 語句為同步導(dǎo)入爆价。
  • 靜態(tài)模塊結(jié)構(gòu)(可以利用于消除無效代碼垦巴,優(yōu)化,靜態(tài)檢查等):導(dǎo)入和導(dǎo)出的關(guān)聯(lián)關(guān)系在運(yùn)行時(shí)不可更改铭段。
  • 在代碼編譯階段(而非執(zhí)行階段) import 語句就和模塊建立了只讀靜態(tài)引用關(guān)系骤宣,且代碼運(yùn)行到import不會執(zhí)行模塊的內(nèi)容,而是當(dāng)導(dǎo)出值被調(diào)用時(shí)才會真正執(zhí)行對應(yīng)模塊序愚。
  • 不能修改 import 進(jìn)來的對象憔披,因?yàn)?code>import/export輸出的模塊是動態(tài)綁定的常量,是只讀的爸吮。但修改對象引用地址的屬性還是可以的芬膝。如無特殊需要請不要這么做。
  • import/export 不能嵌套在任何塊級作用域或函數(shù)作用域內(nèi)形娇,必須寫在模塊頂層(因?yàn)?import 會先于其他任何代碼執(zhí)行)
  • import/export 語句不能有動態(tài)計(jì)算部分
  • 不能直接在瀏覽器執(zhí)行锰霜,需要寫在<script type="module"></script>內(nèi)
  • 自動支持模塊之間的循環(huán)依賴關(guān)系

盡管ESM模塊規(guī)范大有優(yōu)勢,但鑒于很多庫還在廣泛使用CJS埂软,我們?nèi)孕枰斫?code>require和module.exports/exports锈遥。自己在日常開發(fā)中使用importexport default/export即可,webpack 會幫你做兼容處理 (可以看到webpack自身是遵循CJS的勘畔,因此會在打包過程中先把esm轉(zhuǎn)成cjs) 所灸。期待全面支持ESM的一天~

參考:es6 modules

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市炫七,隨后出現(xiàn)的幾起案子爬立,更是在濱河造成了極大的恐慌,老刑警劉巖万哪,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侠驯,死亡現(xiàn)場離奇詭異,居然都是意外死亡奕巍,警方通過查閱死者的電腦和手機(jī)吟策,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來的止,“玉大人檩坚,你說我怎么就攤上這事。” “怎么了匾委?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵拖叙,是天一觀的道長。 經(jīng)常有香客問我赂乐,道長薯鳍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任挨措,我火速辦了婚禮挖滤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘运嗜。我一直安慰自己壶辜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布担租。 她就那樣靜靜地躺著砸民,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奋救。 梳的紋絲不亂的頭發(fā)上岭参,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天,我揣著相機(jī)與錄音尝艘,去河邊找鬼演侯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛背亥,可吹牛的內(nèi)容都是我干的秒际。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼狡汉,長吁一口氣:“原來是場噩夢啊……” “哼娄徊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起盾戴,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤寄锐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后尖啡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橄仆,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年衅斩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盆顾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡畏梆,死狀恐怖椎扬,靈堂內(nèi)的尸體忽然破棺而出惫搏,到底是詐尸還是另有隱情,我是刑警寧澤蚕涤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站铣猩,受9級特大地震影響揖铜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜达皿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一天吓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧峦椰,春花似錦龄寞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滔金,卻和暖如春色解,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背餐茵。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工科阎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忿族。 一個(gè)月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓锣笨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親道批。 傳聞我的和親對象是個(gè)殘疾皇子错英,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容