JavaScript模块化总结(六)

120 阅读6分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。

本文是JavaScript模块化总结系列的最后一篇,我们继续学习ES Module的知识点。

默认导出

在前两篇文章中,我们使用的导出功能都是有名字的导出,那么导入的时候就必须和导出的名字一致。这就需要在导出export时指定名字,在导入import时就需要知道具体的名字。这也是它的缺陷。

在foo.js文件中

const name = 'haha'
const age = 18

export {
  name,
  age
}

在main.js文件中

import {name, age} from './foo.js'

还有一种导出叫默认导出。默认导出export时可以不需要指定名字。在导入时不需要使用{},并且可以自己来指定名字。它也方便我们和现有的commonJS等规范相互操作。

默认导出有两种方式,需要注意的是,在一个模块中,默认导出只能有一个

方式一

export {
  name,
  age,
  foo as default
}

在main.js中,导入的时候不需要使用{},可以自己来指定名字。

import foo from './foo.js'
console.log(foo);

方式二

export default foo

方式二的默认导出比较常用

import函数

通过import加载一个模块,是不可以把它放在逻辑代码中的。

image.png 原因在于:

(1)ES Module在js引擎解析时, 就必须知道它的依赖关系

(2)由于这个时候js代码没有任何运行,所以无法在进行类似于if判断中代码的执行情况;

(3)if判断是在js引擎运行时运行,我们必须到运行时才能确定path的值。而import需要在解析阶段需要知道依赖关系,把解析阶段的东西放在运行阶段的代码里面就会报错。

如果我们想要动态的加载某一个模块,我们可以使用import()函数。

如果是在webpack的环境下:模块化打包工具: es Module Commonjs require

纯ES Module环境下面: import()

脚手架 -》 webpack: import()

let flag = true
if(flag) {
  // import {name} from './modules/foo.js';
  import('./modules/foo.js').then(res => {
    console.log('then中的打印');
    console.log(res.name);
    console.log(res.age);
  }).catch(err => {
    
  })
}

在之前的介绍中,我们可以把import导入看成是一个同步的过程,必须等到需要导入的文件下载加载之后才能执行后续的代码。如果想要后续的代码先执行,可以使用import函数。import函数返回的结果是一个Promise

import('./foo.js').then(res => {
  console.log('res', res.name);
})
console.log(11111);

import meta

在ES11中新增了import.meta。import是一个对象,它有meta属性,meta属性本身也是一个对象。import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如这个模块的url。

我们可以打印一下import.meta。

console.log(import.meta);

image.png

commonJS的加载过程

commonJS模块加载js文件是运行时加载的,并且是同步的

  • 运行时加载意味着是js引擎在执行js代码的过程中加载模块。
  • 同步意味着一个文件没有加载结束之前,后面的代码都不会执行。

image.png commonJS通过module.exports导出的是一个对象。导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量。但是它们指向的是同一个对象,一个变量修改了对象的属性,所有地方都会被修改。

ES Module的解析流程

ES Module加载文件的过程是编译(解析)时加载的,并且是异步的。

  • 编译(解析)时加载,意味着import不能和运行时相关的内容放在一起使用:

比如from后面的路径需要动态获取;比如不能将import放到if等语句的。所以我们有时候也称ES Module是静态分析的,而不是动态或运行时解析的。

异步意味着js引擎在遇到import时会去获取这个文件,但是这个过程是异步的,并不会阻塞主线程继续执行。

浏览器是如何解析ES Module并且让模块之间可以相互引用的呢?

ES Module的解析过程可以划分为3个阶段:

(1)构建。根据地址从服务器中查找js文件,并下载到浏览器中,将这个js文件解析成模块记录(Module Record),这个解析过程是一个静态分析的过程,不会运行js代码。

(2)实例化。根据模块记录创建一个对应的对象,并且分配内存空间。

(3)运行,也就是计算。运行代码,计算值,并且将值填充到内存地址中。注意,导出的时候可以修改变量值,但是导入模块不能修改变变量值。

我们来用代码检验一下。

// foo.js
let name = 'haha'
let age = 18

export {
  name, 
  age
}
// main.js
import {name, age} from './foo.js'
name = 'xixi'
console.log(name);
<!-- index.html -->
<body>
  <script src="./main.js" type="module"></script>
</body>

在本地开启一个服务器,在浏览器中运行。我们可以看到运行报错了,因此可以了解到导入内容时不能修改变量值。

image.png 那么我们在导出时修改变量时会如何呢?

// foo.js
let name = 'haha'
let age = 18

setTimeout(() => {
  name = 'xixi'
  age = 20
}, 1000)

export {
  name,
  age
}
// main.js
import { name, age } from './foo.js'

setTimeout(() => {
  console.log(name);
  console.log(age);
}, 2000)

从结果我们可以看出,两个变量的值都被修改了。 image.png

CommonJS和ES Module是否可以相互引用

什么是相互引用呢?就是我在一个文件中使用CommonJS的module.exports进行导出,那么在另一个文件我是否可以使用ES Module的import进行导入呢?或者我使用ES Module的export进行导出,然后在另一个文件中我是否可以使用CommonJS中的require进行导入呢?

答案是需要区分环境

(1)在浏览器中不能。因为在浏览器环境中默认是不支持CommonJS的,不能在默认的js文件中写module.exports的,它会报module这个变量是没有定义的错误。

(2)在node环境中,需要区分不同的node版本。有一些node版本是不支持ES Module的,node最早是实现Commonjs,后面在一些新的版本中慢慢支持ES Module,但是支持ES Module的有些版本里面需要一些条件,例如在环境中写一些特殊的配置等。

(3)基于webpack的环境里面,例如vue脚手架Vue Cli是基于webpack环境的,react脚手架creat-react-app是基于webpack环境的,它们两个之间是可以任意相互引用的。