Es Module - 深度解析

525 阅读4分钟

函数作用域的缺点

1、函数无法访问在其他函数中定义的变量 image.png

不用担心别的函数会影响到自己作用域里的变量,但是不方便在不同的函数中共享变量,可以使用更上一层的作用域来共享变量,比如去全局作用域。

image.png

这种方式得保证脚本的执行顺序,全局作用域和子函数的依赖关系是不明显的,不利于代码维护,任何子函数都可随意更改全局作用域的变量,这是很危险的。

模块也是一种作用域

模块作用域可用于在模块中的函数之间共享变量。但是与函数作用域不同,模块可以使用 export 明确的说明模块中的变量或函数可用。其他模块可以通过 import 明确的声明他们依赖那些变量和函数。

image.png

因为这是一个明确的关系,所以您可以判断如果删除另一个模块,哪些模块会损坏。

举个例子

// main.js
export let main = 'main'
import { index } from './index.js'
console.log('main', index)

// index.js
import { main } from 'main'
export let index = 'index'

console.log('index', main)

ES 模块如何工作

构建

1. 找出从哪里下载包含模块的文件(又名模块解析)

// 在 html 中
<script src="main.js" type="module">

// 在 js 中
import { count } from './count.js'

2. 获取文件(通过从 URL 下载或从文件系统加载),并添加到模块映射中。

异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

image.png

3. 生成模块记录,并添加到模块映射中。

将获取的文件,解析成一个模块记录,包含了导入和导出的结构信息。

创建模块记录后,将其放置在模块映射中。这意味着无论何时从现在开始请求它,加载器都可以从该映射中获取它。

image.png

整体流程如下:

image.png

  1. 模块会构成一个树

image.png

实例化

执行深度优先后序遍历,从模块依赖图的最底部开始实例化。

1、创建作用域

2、创建模块命名空间对象

key 值为 export 导出的变量名,值为指向该变量分配的内存空间。 其属性可更改,值可变量,不可配置。

image.png

3、创建模块环境记录

管理模块中最顶层的变量,包块模块自己定义的变量,和通过 import 导入的存在于另一个模块记录中的变量。

将 import 进来的变量 N 指向为另一个模块导出的变量 N2 分配的内存空间(这样访问 N 会间接的访问 N2 的值),注意在该阶段,N 和 N2 都没有实际的值,但是会先初始化任何导出的函数声明。

image.png

image.png

image.png

获取 this 会得到 undefined。

如果获取的是一个间接绑定的值,如果该值所在的模块还未初始化,则抛出 ReferenceError 错误

执行代码

这一步主要是执行代码,填充内存空间。

由于模块映射,模块执行也是单例的。与实例化相同,也是深度优先后序遍历执行的。

EsModule 和 CommonJs 的区别

本质区别,EsModule 是语法层,Js引擎做的事,而 CommonJs 是 node 自己实现的模块机制。

CommonJs 从文件系统加载文件,加载时可阻塞主流程,在加载时便进行模块的实例化和执行,意味着在返回模块实例之前,会遍历整个树,加载、实例化和执行任何依赖项。

image.png

CommonJs 可以在模块说明符中使用变量,EsModule 需要在执行前构建整个模块图,不可在模块说明符中使用变量,因为这些变量还没有值。

image.png

在 CommonJS 中,整个导出对象在导出时被复制。这意味着导出的任何值(如数字)都是副本。

image.png

关于循环引用,在 CommonJs 中

image.png

最终会输出 undefined,如果是 Esmodule 会获取到改变后的值。

image.png

一些题

普通的导入导出

// module.js
export let thing = 'initial';
setTimeout(() => { 
    thing = 'changed';
}, 500);

// main.js
import { thing as importedThing } from './module.js';
const module = await import('./module.js');
let { thing } = await import('./module.js');
setTimeout(() => {
    console.log(importedThing); // changed
    console.log(module.thing); // changed
    console.log(thing); // initial
}, 1000);

默认导入导出表达式

// module.js
let thing = 'initial';
export { thing };
export default thing;
setTimeout(() => {
    thing = 'changed';
}, 500);

// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
    console.log(thing); // changed
    console.log(defaultThing); // initial
    console.log(anotherDefaultThing); // initial
 }, 1000);

let thing = 'initial';
export { thing, thing as default };
setTimeout(() => {
    thing = 'changed';
}, 500);


// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
    console.log(thing); // changed
    console.log(defaultThing); // changed
    console.log(anotherDefaultThing); // changed
}, 1000);

默认导出函数

// module.js
export default function thing() {} 
setTimeout(() => {
    thing = 'changed'; 
}, 500);


// main.js
import thing from './module.js';
setTimeout(() => {
    console.log(thing); // changed
}, 1000);

// module.js
function thing() {}
export default thing;
setTimeout(() => {
    thing = 'changed';
}, 500);

// function thing() {}

循环引用

// main.js
import { foo } from './module.js';
foo();
export function hello() {
    console.log('hello');
}

// module.js
import { hello } from './main.js';
hello();
export function foo() {
    console.log('foo'); 
}

// hello foo
// main.js
import { foo } from './module.js';
foo();
export const hello = () => console.log('hello');


// module.js
import { hello } from './main.js';
hello();
export const foo = () => console.log('foo');


export { hello as default }

// 报错

附录

image.png

image.png