接上篇 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}) => {
// ...
});
如上梗脾,export1
和export2
都是my-module.js
用export
導(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ā)中使用import
和export default/export
即可,webpack 會幫你做兼容處理 (可以看到webpack自身是遵循CJS的勘畔,因此會在打包過程中先把esm轉(zhuǎn)成cjs) 所灸。期待全面支持ESM的一天~
參考:es6 modules