介绍
ES6 最大的一个改进就是引入了模块规范,也得到了浏览器的原生支持,它的核心关键字是 import 和 export。
关键字
export
export 关键字导出一个模块中的变量、函数、类等导出,例如:
export const name = 'zayyo'
export function sayHello() {
console.log('hello!')
}
export class Foo{
constructor() {
}
}
当然我们也可以这样做:
class Foo{
constructor() {
}
}
function sayHello() {
console.log('hello!')
}
export {
Foo,
sayHello
}
值得注意的是,export 后面的 {} 并不是所谓对象,只是一个标识符,例如下面的代码就是不合法的(不要和 CommonJS 混淆了):
// 下面的代码不合法!!!
export {
name:'yzl',
age:20
}
另外,我们也可以为导出的内容起个别名:
export {
Foo as FooClass,
sayHello as sayHelloFn
}
import
使用 import 关键字可以从一个模块中导入内容,基于上面的 export 语句,我们来写一下 import:
import {sayHello, Foo} from './foo'
sayHello()
const f = new Foo()
另外,我们可以在 import 时起别名:
import {sayHello as helloFn, Foo as FooClass} from './foo'
helloFn()
const f = new FooClass()
export default
export default 关键字用来实现默认导出,下面是一个案例,注意,在一个模块中只能有一个默认导出:
export default function sayHello() {
console.log('hello!')
}
针对这种默认的导出,我们的 import 语句要这样写:
import sayHello from './xxxx'
sayHello()
import()
值得注意的是,我们的 import 语句是不可以放到逻辑代码里面的(只能放在 js 文件的开头),具体原因后面会说,但有时候我们需要在逻辑判断中动态加载模块,import() 语句用来实现动态加载某一个模块,浏览器原生支持这个功能。
import 和 export 混用
如果你开发一个库,那么你有必要暴露一些接口到库入口中,方便用户调用,例如:
import { Foo } from './scales/foo';
import { Bar } from './scales/bar';
export {
Band,
Ordinal
}
如果 import 很多,代码不太好看,于是我们可以将 import 和 export 混用,像这样:
export { Foo } from './scales/foo';
export { Bar } from './scales/bar';
这份代码和上面的代码效果等价。
特点
先解析后执行
-
和 CommonJS 不同,ES Module 处理依赖关系的时机不是在运行时,而是在解析时(这种行为可以称为静态解析)。
-
这就是为什么上面我们提到 import 不可以直接在代码块中嵌入。
-
同样我们又可以解决一个问题:为什么 import 语句不可以携带变量、而 require 语句可以,这是因为 require 语句是边执行边处理依赖关系。例如下面的代码:
// CommonJS
const a = '/home/app.js'
const res = require(a)
- 在执行第三行时,第二行已经执行,a 变量在内存中,所以 require 有效。
再看 ESModule 的 "实现",注意,下面的代码本身就不合法,只是为了对比。
// ES Module
const a = '/home/app.js'
import {count} from `${a}`
假设浏览器能够解析第2行,在解析阶段,第1行并没有运行,故变量 a 是不存在的,所以这个 import 将无法解析。
异步递归加载解析
所有的模块都是异步递归加载解析的,类似于添加
工作机制
ES Module 的工作机制如下:
构建阶段
构建阶段就是浏览器尝试下载、解析所有需要的模块文件,并形成模块记录的过程,它的基本流程如下:
- 浏览器会解析入口模块,确定依赖,并发送对依赖模块的请求。这些文件通过网络返回后,浏览器就会解析它们的内容、确定它们的依赖,其形式如下:
<script src='./bar.js' type='module'></script>
-
如果这些二级依赖(入口脚本的依赖)还没有加载,则会发送更多请求。这个异步递归加载过程会持续到整个应用程序的依赖图都解析完成。
-
上面的每个模块加载完成之后,都会创建相应的模块记录,同时浏览器还会维护一张模块映射表,它保存了模块路径 -- 模块记录的映射关系。
实例化阶段
上面的构建阶段只是起到了解析模块、将模块转换成模块记录的过程,在实例化阶段,代码和内存将建立起关系。
-
首先,JS 引擎创建一个模块环境记录(module environment record),它管理模块记录中的变量。
-
为每一个 export 在内存中开辟相应的空间,相应的模块环境记录也会指向这些内存空间,现在这些内存空间并没有被填充值,赋值操作将在执行阶段发生。
-
引擎在分析完模块所有的导出之后,开始处理模块的导入,导入和导出都指向内存中相同的位置。
执行阶段
有趣的现象
循环依赖的特性
一个循环依赖的有趣现象:
CommonJS 环境
index.js
let count = require('./counter.js').count
console.log(count)
exports.message = 'hello world!'
counter.js
let message = require('./index.js').message
exports.count = 5
setTimeout(() => {
console.log(message)
}, 0)
上面的代码将输出:
原因:
-
我们先加载
index.js
,执行第一行,进入counter.js
。 -
进入
counter.js
,执行第一行,循环依赖了index.js
。 -
由于
index.js
没有加载完毕,message 的值为 undefined。 -
counter.js
导出了 count 变量。 -
回到
index.js
。 -
打印 count 的值,也就是 5。
-
导出 message 变量,这个已经没什么用了,因为 counter 的 message 已经被解析成 undefined。
-
执行 setTimeout() 中的任务,打印 counter 的 message,结果自然是 undefined。
-
导出 message 变量,这个已经没什么用了,因为 counter 的 message 已经被解析成 undefined。
-
执行 setTimeout() 中的任务,打印 counter 的 message,结果自然是 undefined。
对于 ESModule,是什么效果呢?
index.js
import { count } from './counter.js'
const message = '666'
console.log(count)
export {
message
}
counter.js
import { message } from './index.js'
16const count = 5
18setTimeout(() => {
console.log(message)
}, 0)
export {
count
}
输出如下:
其流程如下:
-
JS 引擎解析模块,分析
import
和export
,进行连接,此时存放导出值的内存(下面简称为导出内存)中还没有被填充值: -
进入执行阶段,先执行
index.js
,index 的 message 被赋值为 5,现在它作为一个局部变量,并未被写入到导出内存中。 -
index.js
需要导入 count 变量,如上图的下半部分,它执行导出内存的 count 部分,由于此时 count 没有被填充,我们自上而下执行counter.js
。 -
在
counter.js
执行到 export 语句时,内存中的 count 被填充为 5。 -
counter.js
继续执行setTimeout()
,console.log(message)
被加入任务队列中。 -
index.js
打印 count,值为 5,接着执行到 export, 将 message 写入导出内存中。 -
主线程代码结束,执行任务队列中的
console.log(message)
,这个 message 是从index.js
导入的,它的值就是 666。
通过这个案例,我们可以更好地理解 ESModule 和 CommonJS 的工作机制。
值得注意的是,如果我们移除包裹的 setTimeout,那么会报错,这个现象进一步证明了上面的描述 -- 只有扫描到 export 语句,相应的变量才会被写入到导出内存中。
export 特点
来看下面的代码:
main.js
//
import {name} from './foo.js'
console.log(name)
setTimeout(() => {
console.log(name)
}, 1000)
foo.js
let name = 'zayyo'
setTimeout(() => {
name = 'zay'
}, 0)
export {
name
}
执行 main.js,得到输出:
如果是 CommonJS 呢?
main.js
const foo = require('./foo.js')
console.log(foo.name)
setTimeout(() => {
console.log(foo.name)
}, 1000)
foo.js
let name = 'zayyo'
setTimeout(() => {
name = 'zay'
}, 0)
module.exports = {
name
}
输出如下:
导出的是 module.exports 的引用,改变 name,由于 name 是一个原始值(不是一个引用类型),和导出的 module.exports 对象的 name 属性并不相同,所以 name 的值不会被修改。
为什么 ESModule 可以跟踪修改呢?这个机制叫实时绑定(live binding),本质上是export 端和 import 端指向内存中的相同位置,和 CommonJS 导出 module.exports 对象的机制是不同的。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情