- 在开始之前,我们先大概了解一下整个流程大概是怎么样的,先有一个大概的了解:
- 阶段一:构建(
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
之后,才能知道当前模块依赖的是那些子模块,然后你需要resolve
子模块,获取子模块,再解析子模块,不断的循环这个流程 resolving -> fetching -> parsing,结果如下图所示:
- 这个过程也称为
静态分析
,不会运行JavaScript代码,只会识别export
和import
关键字,所以说不能在非全局作用域下使用import
,动态导入除外。 - 如果多个文件同时依赖一个文件呢,这会不会引起死循环,答案是不会的。
loader
使用Module Map
对全局的MOdule Record
进行追踪、缓存这样就可以保证模块只被fetch
一次,每个全局作用域中会有一个独立的 Module Map。
MOdule Map 是由一个 URL 记录和一个字符串组成的key/value的映射对象。URL记录是获取模块的请求URL,字符串指示模块的类型(例如。“javascript”)。模块映射的值要么是模块脚本,null(用于表示失败的获取),要么是占位符值“fetching(获取中)”。
linking 链接阶段
- 在所有
Module Record
被解析完后,接下来 JS 引擎需要把所有模块进行链接。JS 引擎以入口文件的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种状态,分别为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
,得出以下结果:
- 好了,这篇文章到这也就结束了。《JavaScript视频教程》