前言
起因是因?yàn)橐淮蝸?lái)回引入引發(fā)的問(wèn)題。例如A引入了B的方法,B引入了A的方法,然后在調(diào)用時(shí)發(fā)現(xiàn)遇到了問(wèn)題,于是決定做一篇關(guān)于模塊引入的整理。
Module語(yǔ)法
歷史上,JavaScript 一直沒(méi)有模塊(module)體系,無(wú)法將一個(gè)大程序拆分成互相依賴的小文件,再用簡(jiǎn)單的方法拼裝起來(lái)。
在 ES6 之前,社區(qū)制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用于服務(wù)器,后者用于瀏覽器。ES6 在語(yǔ)言標(biāo)準(zhǔn)的層面上,實(shí)現(xiàn)了模塊功能,而且實(shí)現(xiàn)得相當(dāng)簡(jiǎn)單,完全可以取代 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案。
ES6 模塊的設(shè)計(jì)思想是盡量的靜態(tài)化,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運(yùn)行時(shí)確定這些東西。比如,CommonJS 模塊就是對(duì)象,輸入時(shí)必須查找對(duì)象屬性。
require和import本質(zhì)上就是commonjs和ES6module的區(qū)別。一個(gè)是JavaScript社區(qū)指定的野生規(guī)范,一個(gè)是后續(xù)指定的官方規(guī)范。
// 常規(guī)用法
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// ES6模塊
import { stat, exists, readFile } from 'fs';
import的一些注意事項(xiàng)
import命令輸入的變量都是只讀的,因?yàn)樗谋举|(zhì)是輸入接口。也就是說(shuō),不允許在加載模塊的腳本里面,改寫(xiě)接口。
// 報(bào)錯(cuò)
import { 'f' + 'oo' } from 'my_module';
// 報(bào)錯(cuò)
let module = 'my_module';
import { foo } from module;
// 報(bào)錯(cuò)
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
由于import是靜態(tài)執(zhí)行,所以不能使用表達(dá)式和變量,這些只有在運(yùn)行時(shí)才能得到結(jié)果的語(yǔ)法結(jié)構(gòu)。
因此也不存在著頁(yè)面根據(jù)條件判斷去import資源這種說(shuō)法。
import會(huì)執(zhí)行所加載模塊。例如:import 'lodash',僅僅執(zhí)行l(wèi)odash模塊,但是不輸入任何值。
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
目前階段,通過(guò) Babel 轉(zhuǎn)碼,CommonJS 模塊的require命令和 ES6 模塊的import命令,可以寫(xiě)在同一個(gè)模塊里面,但是最好不要這樣做。因?yàn)?strong>import在靜態(tài)解析階段執(zhí)行,所以它是一個(gè)模塊之中最早執(zhí)行的。下面的代碼可能不會(huì)得到預(yù)期結(jié)果。
關(guān)于import()函數(shù),其誕生的目的是為了讓import也有動(dòng)態(tài)加載的能力,實(shí)現(xiàn)require先前擁有的基本功能。
關(guān)于require和import本質(zhì)區(qū)別
寸志:形式上看上去五花八門(mén),本質(zhì)可以涵蓋為以下三點(diǎn)。
- CommonJS 還是 ES6 Module 輸出都可以看成是一個(gè)具備多個(gè)屬性或者方法的對(duì)象;
- default 是 ES6 Module 所獨(dú)有的關(guān)鍵字,export default fs 輸出默認(rèn)的接口對(duì)象,import fs from 'fs' 可直接導(dǎo)入這個(gè)對(duì)象;
- ES6 Module 中導(dǎo)入模塊的屬性或者方法是強(qiáng)綁定的,包括基礎(chǔ)類型;而 CommonJS 則是普通的值傳遞或者引用傳遞。
第3點(diǎn)具體該如何理解?
// counter.js
exports.count = 0
setTimeout(function () { ++exports.count }, 500)
// commonjs.js
const {count} = require('./counter')
setTimeout(function () {
console.log('read count after 1000ms in commonjs is', count)
}, 1000)
// 輸出結(jié)果:read count after 1000ms in commonjs is 0
//es6.js
import {count} from './counter'
setTimeout(function () {
console.log('read count after 1000ms in es6 is', count)
}, 1000)
// 輸出結(jié)果:read count after 1000ms in es6 is 1
count為number類型,因此commonjs中是值傳遞,傳遞0之后后續(xù)原來(lái)的參數(shù)發(fā)生變化并不會(huì)影響到引用方的count值。而es6中為強(qiáng)綁定,因此后續(xù)也跟著發(fā)生了改變。
關(guān)于循環(huán)引用
解答這個(gè)問(wèn)題需要先了解commonjs和es6底層的加載原理
commonjs
CommonJS 的一個(gè)模塊,就是一個(gè)腳本文件。require命令第一次加載該腳本,就會(huì)執(zhí)行整個(gè)腳本,然后在內(nèi)存生成一個(gè)對(duì)象。
{
id: '...', // 模塊名
exports: { ... }, // 模塊輸出的各個(gè)接口
loaded: true, // 表示是否執(zhí)行完畢
...
}
執(zhí)行一次后,再次執(zhí)行只會(huì)去exports上取值,返回第一次運(yùn)行的結(jié)果。
CommonJS 模塊遇到循環(huán)加載時(shí),返回的是當(dāng)前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值,兩者可能會(huì)有差異。所以,輸入變量的時(shí)候,必須非常小心。
es6
ES6 模塊是動(dòng)態(tài)引用,如果使用import從一個(gè)模塊加載變量(即import foo from 'foo'),那些變量不會(huì)被緩存,而是成為一個(gè)指向被加載模塊的引用,需要開(kāi)發(fā)者自己保證,真正取值的時(shí)候能夠取到值。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
直接拋出這個(gè)典型的例子。執(zhí)行a.mjs后,引用b,已知它從a輸入了foo接口,這時(shí)不會(huì)去執(zhí)行a,而是認(rèn)為這個(gè)接口已經(jīng)存在了。因此執(zhí)行到第三行時(shí)發(fā)現(xiàn)foo并沒(méi)有定義ReferenceError。
可以通過(guò)將bar,foo寫(xiě)成函數(shù)的形式,這是因?yàn)楹瘮?shù)具有提升作用,在執(zhí)行import {bar} from './b'時(shí),函數(shù)foo就已經(jīng)有定義了,所以b.mjs加載的時(shí)候不會(huì)報(bào)錯(cuò)。這也意味著,如果把函數(shù)foo改寫(xiě)成函數(shù)表達(dá)式,也會(huì)報(bào)錯(cuò)。
心得體會(huì)
關(guān)于import的討論,先前分析過(guò)它預(yù)加載、動(dòng)態(tài)加載的行為。本次熟悉了語(yǔ)法用法和加載的機(jī)制,盡量通過(guò)函數(shù)的形式去返回可能存在循環(huán)調(diào)用的模塊,在發(fā)生循環(huán)調(diào)用時(shí)也要尤為警惕避免產(chǎn)生不可預(yù)見(jiàn)的bug。
參考
Module 的語(yǔ)法 - 阮老師的es6教程
Module 的加載實(shí)現(xiàn) - 阮老師的es6教程
require,import區(qū)別?-知乎