本篇内容介绍了“es6模块化如何使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
天下苦 CommonJs 久矣
- 
        
 
 的独特之处在于,既可以通过浏览器原生加载,也可以与第三方加载器和构建工具一起加载。Es Module
 
- 
        支持 
 模块的浏览器可以从顶级模块加载整个依赖图,且是异步完成。浏览器会解析入口模块,确定依赖,并发送对依赖模块的请求。这些文件通过网络返回后,浏览器就会解析它们的依赖,,如果这些二级依赖还没有加载,则会发送更多请求。Es module
 
- 
        这个异步递归加载过程会持续到整个应用程序的依赖图都解析完成。解析完成依赖图,引用程序就可以正式加载模块了。 
- 
        
 
 不仅借用了Es Module
 和CommonJs
 的很多优秀特性,还增加了一些新行为:AMD
 
- 
        
 
 默认在严格模式下执行;Es Module
 
- 
        
 
 不共享全局命名空;Es Module
 
- 
        
 
 顶级的Es Module
 的值是this
 (常规脚本是undefined
 );window
 
- 
        模块中的 
 声明不会添加到var
 对象;window
 
- 
        
 
 是异步加载和执行的;Es Module
 
export 和 import
- 
        模块功能主要由两个命令构成: 
 和exports
 。import
 
- 
        
 
 命令用于规定模块的对外接口,export
 命令用于输入其他模块提供的功能。import
 
export的基本使用
- 
        导出的基本形式: 
export const nickname = "moment";
export const address = "广州";
export const age = 18; - 
        当然了,你也可以写成以下的形式: 
const nickname = "moment";
const address = "广州";
const age = 18;
export { nickname, address, age }; - 
        对外导出一个对象和函数 
export function foo(x, y) {
  return x + y;
}
export const obj = {
  nickname: "moment",
  address: "广州",
  age: 18,
};
// 也可以写成这样的方式
function foo(x, y) {
  return x + y;
}
const obj = {
  nickname: "moment",
  address: "广州",
  age: 18,
};
export { foo, obj }; - 
        通常情况下, 
 输出的变量就是本来的名字,但是可以使用export
 关键字重命名。as
 
const address = "广州";
const age = 18;
export { nickname as name, address as where, age as old }; - 
        默认导出,值得注意的是,一个模块只能有一个默认导出: 
export default "foo";
export default { name: 'moment' }
export default function foo(x,y) {
  return x+y
}
export { bar, foo as default }; export 的错误使用
- 
        导出语句必须在模块顶级,不能嵌套在某个块中: 
if(true){
export {...};
} - 
        
 
 必须提供对外的接口:export
 
// 1只是一个值,不是一个接口export 1// moment只是一个值为1的变量const moment = 1export moment// function和class的输出,也必须遵守这样的写法function foo(x, y) {    return x+y
}export foo复制代码 import的基本使用
- 
        使用 
 命令定义了模块的对外接口以后,其他js文件就可以通过export
 命令加载整个模块import
 
import {foo,age,nickname} from '模块标识符' - 
        模块标识符可以是当前模块的相对路径,也可以是绝对路径,也可以是纯字符串,但不能是动态计算的结果,例如凭借的字符串。 
- 
        
 
 命令后面接受一个花括弧,里面指定要从其他模块导入的变量名,而且变量名必须与被导入模块的对外接口的名称相同。import
 
- 
        对于导入的变量不能对其重新赋值,因为它是一个只读接口,如果是一个对象,可以对这个对象的属性重新赋值。导出的模块可以修改值,导入的变量也会跟着改变。 
- 
        从上图可以看得出来,对象的属性被重新赋值了,而变量的则报了 
 的类型错误。Assignment to constant variable
 
- 
        如果模块同时导出了命名导出和默认导出,则可以在 
 语句中同时取得它们。可以依次列出特定的标识符来取得,也可以使用import
 来取得:*
 
// foo.js
export default function foo(x, y) {
  return x + y;
}
export const bar = 777;
export const baz = "moment";
// main.js
import { default as foo, bar, baz } from "./foo.js";
import foo, { bar, baz } from "./foo.js";
import foo, * as FOO from "./foo.js"; 动态 import
- 
        标准用法的 
 导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。import
 
- 
        关键字 
 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个import
 。promise
 
import("./foo.js").then((module) => {  const { default: foo, bar, baz } = module;  console.log(foo); // [Function: foo]
  console.log(bar); // 777
  console.log(baz); // moment});复制代码 使用顶层 await
- 
        在经典脚本中使用 
 必须在带有await
 的异步函数中使用,否则会报错:async
 
import("./foo.js").then((module) => {
  const { default: foo, bar, baz } = module;
  console.log(foo); // [Function: foo]
  console.log(bar); // 777
  console.log(baz); // moment
}); - 
        而在模块中,你可以直接使用 
 :Top-level await
 
const p = new Promise((resolve, reject) => {  resolve(777);
});const result = await p;console.log(result); 
// 777正常输出 import 的错误使用
- 
        由于 
 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。import
 
// 错误
import { 'b' + 'ar' } from './foo.js';
// 错误
let module = './foo.js';
import { bar } from module;
// 错误
if (x === 1) {
  import { bar } from './foo.js';
} else {
  import { foo } from './foo.js';
} 在浏览器中使用 Es Module
- 
        在浏览器上,你可以通过将 
 属性设置为type
 用来告知浏览器将module
 标签视为模块。script
 
<script type="module" src="./main.mjs"></script><script type="module"></script> - 
        模块默认情况下是延迟的,因此你还可以使用 
 的方式延迟你的defer
 脚本:nomodule
 
  <script type="module">      
  console.log("模块情况下的");
    </script>    
    <script src="./main.js" type="module" defer></script>
    <script>
      console.log("正常 script标签");    
      </script> - 
        在浏览器中,引入相同的 
 脚本会被执行多次,而模块只会被执行一次:nomodule
 
    <script src="./foo.js"></script>    <script src="./foo.js"></script>
    <script type="module" src="./main.js"></script>
    <script type="module" src="./main.js"></script>
    <script type="module" src="./main.js"></script> 模块的默认延迟
- 
        默认情况下, 
 脚本会阻塞nomodule
 解析。你可以通过添加HTML
 属性来解决此问题,该属性是等到defer
 解析完成之后才执行。HTML
 
- 
        
 
 和defer
 是一个可选属性,他们只可以选择其中一个,在async
 脚本下,nomodule
 等到defer
 解析完才会解析当前脚本,而HTML
 会和async
 并行解析,不会阻塞HTML
 的解析,模块脚本可以指定HTML
 属性,但对于async
 无效,因为模块默认就是延迟的。defer
 
- 
        对于模块脚本,如果存在 
 属性,模块脚本及其所有依赖项将于解析并行获取,并且模块脚本将在它可用时进行立即执行。async
 
Es Module 和 Commonjs 的区别
- 
        讨论 
 模块之前,必须先了解Es Module
 与Es Module
 完全不同,它们有三个完全不同:Commonjs
 
- 
        
 
 模块输出的是一个值的拷贝,CommonJS
 输出的是值的引用;Es Module
 
- 
        
 
 模块是运行时加载,CommonJS
 是编译时输出接口。Es Module
 
- 
        
 
 模块的CommonJS
 是同步加载模块,ES6 模块的require()
 命令是异步加载,有一个独立的模块依赖的解析阶段。import
 
- 
        第二个差异是因为 
 加载的是一个对象(即CommonJS
 属性),该对象只有在脚本运行完才会生成。而module.exports
 不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。Es Module
 
- 
        
 
 输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。具体可以看上一篇写的文章。Commonjs
 
- 
        
 
 的运行机制与Es Module
 不一样。CommonJS
 对脚本静态分析的时候,遇到模块加载命令JS引擎
 ,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,import
 就是一个连接管道,原始值变了,import
 加载的值也会跟着变。因此,import
 是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。Es Module
 
Es Module 工作原理的相关概念
- 
        在学习工作原理之前,我们不妨来认识一下相关的概念。 
Module Record
- 
        模块记录( 
 ) 封装了关于单个模块(当前模块)的导入和导出的结构信息。此信息用于链接连接模块集的导入和导出。一个模块记录包括四个字段,它们只在执行模块时使用。其中这四个字段分别是:Module Record
 
- 
        
 
 : 创建当前模块的作用域;Realm
 
- 
        
 
 :模块的顶层绑定的环境记录,该字段在模块被链接时设置;Environment
 
- 
        
 
 :模块命名空间对象是模块命名空间外来对象,它提供对模块导出绑定的基于运行时属性的访问。模块命名空间对象没有构造函数;Namespace
 
- 
        
 
 :字段保留,以按HostDefined
 使用,需要将附加信息与模块关联。host environments
 
Module Environment Record
- 
        模块环境记录是一种声明性环境记录,用于表示ECMAScript模块的外部作用域。除了普通的可变和不可变绑定之外,模块环境记录还提供了不可变的 
 绑定,这些绑定提供了对存在于另一个环境记录中的目标绑定的间接访问。import
 
不可变绑定就是当前的模块引入其他的模块,引入的变量不能修改,这就是模块独特的不可变绑定。
Es Module 的解析流程
- 
        在开始之前,我们先大概了解一下整个流程大概是怎么样的,先有一个大概的了解: 
- 
        阶段一:构建( 
 ),根据地址查找Construction
 文件,通过网络下载,并且解析模块文件为js
 ;Module Record
 
- 
        阶段二:实例化( 
 ),对模块进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址;Instantiation
 
- 
        阶段三:运行( 
 ),运行代码,计算值,并且将值填充到内存地址中;Evaluation
 
Construction 构建阶段
- 
        
 
 负责对模块进行寻址及下载。首先我们修改一个入口文件,这在loader
 中通常是一个HTML
 的标签来表示一个模块文件。<script type="module"></script>
 
- 
        模块继续通过 
 语句声明,在import
 声明语句中有一个 模块声明标识符(import
 ),这告诉ModuleSpecifier
 怎么查找下一个模块的地址。loader
 
- 
        每一个模块标识号对应一个 
 ,而每一个模块记录(Module Record)
 包含了模块记录
 、JavaScript代码
 、执行上下文
 、ImportEntries
 、LocalExportEntries
 、IndirectExportEntries
 。其中StarExportEntries
 值是一个ImportEntries
 类型,而ImportEntry Records
 、LocalExportEntries
 、IndirectExportEntries
 是一个StarExportEntries
 类型。ExportEntry Records
 
ImportEntry Records
- 
        一个 
 包含三个字段ImportEntry Records
 、ModuleRequest
 、ImportName
 ;LocalName
 
- 
        ModuleRequest: 一个模块标识符( 
 );ModuleSpecifier
 
- 
        ImportName: 由 
 模块标识符的模块导出所需绑定的名称。值ModuleRequest
 表示导入请求是针对目标模块的命名空间对象的;namespace-object
 
- 
        LocalName: 用于从导入模块中从当前模块中访问导入值的变量; 
- 
        详情可参考下图: 
- 
        下面这张表记录了使用 
 导入的import
 字段的实例:ImportEntry Records
 
| 导入声明 (Import Statement From) | 模块标识符 (ModuleRequest) | 导入名 (ImportName) | 本地名 (LocalName) | 
|---|---|---|---|
| import React from "react"; | "react" | "default" | "React" | 
| import * as Moment from "react"; | "react" | namespace-obj | "Moment" | 
| import {useEffect} from "react"; | "react" | "useEffect" | "useEffect" | 
| import {useEffect as effect } from "react"; | "react" | "useEffect" | "effect" | 
ExportEntry Records
- 
        一个 
 包含四个字段ExportEntry Records
 、ExportName
 、ModuleRequest
 、ImportName
 ,和LocalName
 不同的是多了一个ImportEntry Records
 。ExportName
 
- 
        ExportName: 此模块用于导出时绑定的名称。 
- 
        下面这张表记录了使用 
 导出的export
 字段的实例:ExportEntry Records
 导出声明 导出名 模块标识符 导入名 本地名 export var v; "v" null null "v" export default function f() {} "default" null null "f" export default function () {} "default" null null "default" export default 42; "default" null null "default" export {x}; "x" null null "x" export {v as x}; "x" null null "v" export {x} from "mod"; "x" "mod" "x" null export {v as x} from "mod"; "x" "mod" "v" null export * from "mod"; null "mod" all-but-default null export * as ns from "mod"; "ns "mod" all null 
- 
        回到主题 
- 
        只有当解析完当前的 
 之后,才能知道当前模块依赖的是那些子模块,然后你需要Module Record
 子模块,获取子模块,再解析子模块,不断的循环这个流程 resolving -> fetching -> parsing,结果如下图所示:resolve
 
- 
        这个过程也称为 
 ,不会运行JavaScript代码,只会识别静态分析
 和export
 关键字,所以说不能在非全局作用域下使用import
 ,动态导入除外。import
 
- 
        如果多个文件同时依赖一个文件呢,这会不会引起死循环,答案是不会的。 
- 
        
 
 使用loader
 对全局的Module Map
 进行追踪、缓存这样就可以保证模块只被MOdule Record
 一次,每个全局作用域中会有一个独立的 Module Map。fetch
 
MOdule Map 是由一个 URL 记录和一个字符串组成的key/value的映射对象。URL记录是获取模块的请求URL,字符串指示模块的类型(例如。“javascript”)。模块映射的值要么是模块脚本,null(用于表示失败的获取),要么是占位符值“fetching(获取中)”。
linking 链接阶段
- 
        在所有 
 被解析完后,接下来 JS 引擎需要把所有模块进行链接。JS 引擎以入口文件的Module Record
 作为起点,以深度优先的顺序去递归链接模块,为每个Module Record
 创建一个Module Record
 ,用于管理Module Environment Record
 中的变量。Module Record
 
- 
        
 
 中有一个Module Environment Record
 ,这个是用来存放Binding
 导出的变量,如上图所示,在该模块Module Record
 处导出了一个main.js
 的变量,在count
 中的Module Environment Record
 就会有一个Binding
 ,在这个时候,就相当于count
 的编译阶段,创建一个模块实例对象,添加相对应的属性和方法,此时值为V8
 或者undefined
 ,为其分配内存空间。null
 
- 
        而在子模块 
 中使用了count.js
 关键字对import
 进行导入,而main.js
 的count.js
 和import
 的main.js
 的变量指向的内存位置是一致的,这样就把父子模块之间的关系链接起来了。如下图所示:export
 
- 
        需要注意的是,我们称 
 导出的为父模块,export
 引入的为子模块,父模块可以对变量进行修改,具有读写权限,而子模块只有读权限。import
 
Evaluation 求值阶段
- 
        在模块彼此链接完之后,执行对应模块文件中顶层作用域的代码,确定链接阶段中定义变量的值,放入内存中。 
Es module 是如何解决循环引用的
- 
        在 
 中有5种状态,分别为Es Module
 、unlinked
 、linking
 、linked
 和evaluating
 ,用循环模块记录(evaluated
 )的Cyclic Module Records
 字段来表示,正是通过这个字段来判断模块是否被执行过,每个模块只执行一次。这也是为什么会使用Status
 来进行全局缓存Module Map
 的原因了,如果一个模块的状态为Module Record
 ,那么下次执行则会自动跳过,从而包装一个模块只会执行一次。evaluated
 采用Es Module
 的方法对模块图进行遍历,每个模块只执行一次,这也就避免了死循环的情况了。深度优先
 
深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
- 
        看下面的例子,所有的模块只会运行一次: 
// main.js
import { bar } from "./bar.js";
export const main = "main";
console.log("main");
// foo.js
import { main } from "./main.js";
export const foo = "foo";
console.log("foo");
// bar.js
import { foo } from "./foo.js";
export const bar = "bar";
console.log("bar"); - 
        通过 
 运行node
 ,得出以下结果:main.js