ESModule规范详解
在介绍 ESModule
规范之前,我们先了解下 AMD
和 CMD
两种规范。
AMD 规范
- AMD 规范采用非同步加载模块,允许指定回调函数;
- node 模块通常位于本地,加载速度快,所以适用于同步加载;
- 浏览器环境下,模块需要远程请求获取,所以适用于异步;
- require.js 是 AMD 的一个具体实现库;
CMD 规范
- CMD 整合了Commonjs 和 AMD 的优点,模块加载是异步的
- CMD 专门用于浏览器端,sea.js 是 CMD 规范的实现
AMD 和 CMD 最大的问题是没有通过语法升级来解决模块化的问题。它们去定义模块化还是调用js方法的方式去生成一个模块,如果当项目模块达到成百上千个,这种方式无法进行模块规模化的应用。要想模块规模化应用则需要一种标准的语法规范,这是 AMD 和 CMD 都没有实现的。
ESModule 规范
- ESModule 设计理念是希望在编译时就确定模块依赖关系即输入输出
- Commonjs 和 AMD 必须在运行时才能确定依赖和输入、输出
- ESModule 通过 import 加载模块,通过 export 输出模块
下面我们来详细介绍ESModule。
ESModule 使用
export 正常导出,import 导入
所有通过 export 导出的属性,在 import 中可以通过解构的方式获取。
export 导出
const name = 'dog'
const author = 'xiaoming'
export { name, author }
export const say = function () { console.log('hello , world') }
import 导入
// name , author , say 对应 a.js 中的 name , author , say
import { name , author , say } from './a.js'
默认导出 export default
const name = 'dog'
const author = 'xiaoming'
const say = function () {
console.log('hello , world')
}
export default {
name,
author,
say
}
导入
import mes from './a.js'
console.log(mes) // { name: 'dog', ... }
- export default anything 默认导出。 anything 可以是函数,属性方法,或者对象。
- 对于引入默认导出的模块,import anyName from 'module', anyName 可以是自定义名称。
混合导入|导出
export const name = 'dog'
export const author = 'xiaoming'
export default function say() {
console.log('hello , world')
}
导入有两种方式,第一种是:
import theSay, { name, author as bookAuthor } from './a.js'
console.log(
theSay, // ƒ say() {console.log('hello , world') }
name, // 'dog'
bookAuthor // 'xiaoming'
)
第二种:
import theSay, * as mes from './a'
console.log(
theSay, // ƒ say() { console.log('hello , world') }
mes
)
mes对象如下,可以看到把导出的所有属性都收敛到了一个对象里面,其中 export default 导出值的key 为 default
。
{
name: 'dog' ,
author: 'xiaoming',
default: ƒ say() { console.log('hello , world') }
}
ESModule 特点
静态语法
ESModule 的设计理念是希望在编译时就确定模块依赖关系即输入输出,那如何在编译时就能确定依赖关系呢?
在传统编译语言的流程中,程序中的一段源代码在执行之前都需要经过"编译"。对于 JavaScript 这样的解释型语言来说,也是需要编译的,只不过编译过程发生在代码执行前的几微秒(甚至更短)的时间内。
要想在编译阶段就能确定依赖关系,那必须要把import
进行类似于变量提升。我们来看一下JavaScript 中的变量提升。
function test() {
console.log(a)
console.log(foo())
var a = 1
function foo() {
return 2
}
}
test()
在编译阶段发生了变量提升,经过预编译,执行顺序就变成了这样:
function test() {
function foo() {
return 2
}
var a
console.log(a)
console.log(foo())
a = 1
}
test()
所以打印结果是:
undefined
2
import
提升
其实 JavaScript 代码在编译阶段发现有 import
也会像 var
一样进行提升。为了验证这一点,看一下如下 demo。
main.js
console.log('main.js开始执行')
import say from './a.js'
import say1 from './b.js'
console.log('main.js执行完毕')
a.js
import b from './b.js'
console.log('a模块加载')
export default function say() {
console.log('hello , world')
}
b.js
console.log('b模块加载')
export default function sayhello(){
console.log('hello,world')
}
执行顺序如下:
b模块加载
a模块加载
main.js开始执行
main.js执行完毕
当执行main.js
,可以看到先答应"b模块加载",但是 main.js
第一行代码是console.log('main.js开始执行')
, 但是并没有执行。这是因为在编译阶段把import进行了提升,类似于var的变量提升,所以首先执行import
语句。
也就是在编译阶段去加载模块,然后在执行阶段就去执行文件,这跟var变量的执行顺序是一样的,即首先会把var a = undefined
提升,然后在执行阶段去赋值。
因为这种静态语法,所以import
, export
不能放在块级作用域或条件语句中。
// 错误写法
function say() {
import name from './a.js'
export const author = 'dog'
}
// 错误写法
isexport && export const name = '《React进阶实践指南》'
在编译过程中确定了导入和导出的关系,所以更方便去查找依赖,这为tree shaking
(摇树)创造了条件,这也是 ESModule 支持tree-shaking
操作的原因。同时,还可以使用各种 lint 工具对模块依赖进行检查,比如: eslint。
导出绑定: 不能修改import导入的属性
// a.js
export let num = 1
export const addNumber = () => {
num++
}
// main.js
import { num, addNumber } from './a'
num = 2
当执行 main.js
的时候会报错:Uncaught TypeError: Assignment to constant variable。通过 import 导入的值可以看出是一个 const 常量,不能修改。从这里其实可以看出导出的变量num
并不是值的拷贝,而是引用关系。
引用传递
Common.js 是值的拷贝,ESModule 是引用传递。
Common.js
// test.js
let a = 1
function plus() {
a++
}
function get() {
return a
}
exports.a = a
exports.plus = plus
exports.get = get
// main.js
const { a, plus, get } = require('./test.js')
console.log(a) // 1
plus()
console.log(a) // 1
console.log(get()) // 2
当第一次打印导入的变量a
的值是 1,然后执行plus方法,再次打印a
发现值仍然是 1。当我们通过get方法获取模块内的变量a
的时候,发现值为2。所以,在Commonjs规范下,导入的变量只是值的拷贝而已,具体细节可以参考上一篇文章# CommonJS规范详解。
ESModule
// test.js
export let a = 1
export function plus() {
a++
}
// main.js
import { a, plus } from './test.js'
console.log(a) // 1
plus()
console.log(a) // 2
当第一次打印导入的变量a
的值是 1,然后执行plus方法,再次打印a
发现值是 2。也就是,使用 import 导入的变量是与原变量是引用关系,而不是拷贝。
import() 动态引入
import() 返回一个 Promise 对象, 返回的 Promise 的 then 成功回调中,可以获取模块的加载成功信息。我们来简单看一下 import() 是如何使用的。
// main.js
const result = import('./b')
result.then(res => {
console.log(res)
})
// test.js
export const name = 'alien'
export default function sayhello() {
console.log('hello,world')
}
打印结果如下:
Commonjs VS ESModule
通过上面的介绍,我们来总结下 Commonjs 和 ESModule 的区别:
- Commonjs 的输出是值的拷贝,ESModule 的输出是值的引用。
- Commonjs 是运行时加载,只有在运行结束后才能确定输入和输出,ESModule 在编译的时候就能明确的知道输入和输出,它是一个确定的结果。
像这段代码在仅仅被 parse 成 AST(抽象语法树) 时,很难分析出究竟依赖了哪些模块。
const bar = require(`all/${['f', 'o', 'o'].join('')}`)
const foo = _.get(require('all'), 'foo')
同样,Commonjs 在做模块导出时也无法静态识别:
module.exports = {}
Object.assign(module.exports, {bar: 'foo'})
但是在 ESModule 中,import/exports 一目了然,对于没有被 import 的部分,也很自然的容易区分出来。总之,就是通过限定语法,让本来需要运行代码才能确定的依赖,可以在 AST 阶段就能确定下来。
转载自:https://juejin.cn/post/7098537945103073316