ESModule 加载与运行机制

简介: ESModule 作为 JS 的标准模块机制,在日常开发中被广泛使用,但在大部分情况下,我们可能只是将其作为 JS 代码文件的组织形式来对待。作为 JS 的模块规范,ESModule 底层其实有一套非常完善的机制,来确保 ESModule 在不同场景下的性能以及行为的确定性。本文的主要内容是关于 ESModule 加载运行的相关原理和机制的分享,在理解了相关的原理和机制之后,你将会对平常在使用 ESModule 过程中遇到的一些问题(比如:循环引用在什么情况下会报错、TreeShaking 的原理等)有更加深入的理解。

1.gif

ESModule 作为 JS 的标准模块机制,在日常开发中被广泛使用,但在大部分情况下,我们可能只是将其作为 JS 代码文件的组织形式来对待。作为 JS 的模块规范,ESModule 底层其实有一套非常完善的机制,来确保 ESModule 在不同场景下的性能以及行为的确定性。本文的主要内容是关于 ESModule 加载运行的相关原理和机制的分享,在理解了相关的原理和机制之后,你将会对平常在使用 ESModule 过程中遇到的一些问题(比如:循环引用在什么情况下会报错、TreeShaking 的原理等)有更加深入的理解。


从一个循环依赖例子说起


下面用一个包含循环引用的例子来分享 ESModule 的加载和执行过程

// main.mjs
import { mod1Fn } from './mod1.mjs'
import { mod2Fn } from './mod2.mjs'
mod1Fn('main')
mod2Fn('main')
// mod1.mjs
import './mod2.mjs'
export let mod1Value = 'mod1Value'
export function mod1Fn(from) {
  console.log(`${from} call mod1Fn\n`)
}
// mod2.mjs
import { mod1Fn, mod1Value } from './mod1.mjs'
export function mod2Fn(from) {
  console.log(`${from} call mod1Fn\n`)
  console.log('log mod1Value in mod2Fn')
  console.log(mod1Value)
}
mod1Fn('mod2')
mod2Fn('mod2')

图片.png

以上的代码内容分别描述了 main.mjs、mod1.mjs、mod2.mjs  3 个文件的内容,下面我们通过 node 来运行以上的代码,执行 node index.mjs ,输出结果如下


image.gif图片.png


可以看出循环引用在 ESModule 中是可用的,但如果我们在 mod2.mjs 中增加一行调用 mod2Fn 的代码,如下所示:


image.gif图片.png


然后再执行代码,会发现实际执行会报错:


image.gif图片.png


以上的报错是否似曾相识,从报错的信息我们大致可以推断出这个是一个和变量提升有关的报错,实际上报错的根因是在执行 mod2Fn('mod2') 时,mod1Value 还没有完成初始化,类似是下面这样的情况:


console.log(mod1Value)
let mod1Value = 'mod1Value'


但是从直观上看,mod2.mjs 是在 mod1.mjs 之后加载的,为什么 mod1Value 会没有初始化呢,会不会是因为 import 的位置在 mod1Value 之前的原因,导致没有完成初始化呢,但实际上,即使将 mod2 的 import 后置,比如以下的代码

image.gif图片.png


仍然还是会报错,因此我们可以得出报错的原因和 import 的位置是无关的。实际上 ESModule 的加载和执行过程并不是简单的一个按顺序执行的流程,下面我们从底层原理的角度,分享一下 ESModule 实际是如何被加载和执行的,以及会出现以上报错的原因。


ESModule 加载和执行过程解析


ESModule 的加载和解析过程整体上可以拆分为三个步骤:


  1. 获取代码和解析:建立模块之间连接
  2. 实例化模块:完成变量声明和环境对象(enviroment object)的绑定
  3. 执行代码:按照深度优先的顺序,逐行执行代码


下面我们还是以上面的代码为例,分享实际的过程


 获取代码和解析:建立模块之间连接

首先浏览器或者 Node 等应用程序会通过网络请求或文件读写等形式获取到对应的 ESModule 的代码,比如上文中的代码node main.mjsNode 会逐步执行以下操作:


  1. 通过文件读写的方式,读取 main.mjs 的文件内容,记录到 ESModule 中
  2. 逐行解析 JS 代码,获取依赖的 ESModule 的地址
  3. 然后继续加载对应依赖的模块,重复第一步的操作,直到所有的 ESModule 都完成了加载


在完成这一步之后,我们会得到下面这样一张图


图片.png


得关注的是,以上的这些过程 JS 代码还没有被执行,是通过解析代码文本的方式,完成了模块的依赖解析和加载,以及建立模块之间的依赖关系。


 实例化模块:完成声明和环境对象(enviroment object)的绑定

上面一步完成了模块之间的依赖关系生成,接下来实例化本质上是更进一步,完成每个模块内部的变量的声明以及构建模块间的引用关系。在这个过程中有几个要注意的点:


  1. 每个模块都会有各自环境对象且相互隔离,这也是不同模块可以有相同的名字的函数、变量而不会冲突的原因
  2. function、var 的变量提升的特性在这个场景下也适用,function 会直接完成初始化,var 则会初始化为 undefined


还是以上面的例子为例,完成实例化之后我们会得到以下的依赖关系,具体的结果可见下图:


image.gif图片.png


绑定的过程会有两种形式:


  1. 使用 import 的方式引入的,则会在当前模块生成一个间接绑定,指向对应的来源对象,比如 index.mjs 中的 mod1Fn
  2. 如果是在当前模块声明的,则直接绑定到当前的对象,比如 mod1.mjs 中的 mod1Fn


同样在这一个步骤,JS 代码仍然没有进入执行阶段

 执行代码:按照深度优先的顺序,逐行执行代码

接下来进入实际的执行代码阶段,也是 JS 引擎开始执行代码的时机。整体的执行策略会遵循两个大的规则:


  1. 按照深度优先的顺序,首先执行最深的依赖的模块代码
  2. 每个模块的代码只会被执行一次

image.gif图片.png


由第1阶段的依赖关系图,我们可以看出,依赖最深的是 mod2.mjs,所有 JS 引擎会先执行 mod2.mjs 中的代码,即:


image.gif图片.png

然后根据第 2 阶段实例化代码得的到绑定关系图,会先执行以下红框中的部分


image.gif图片.png


从上往下依次执行 mod2 中的代码,在执行到 12 行 mod2Fn('mod2') 时,mod2Fn 在第 7 行依赖了 mod1Value,而由上图我们可以看到,mod1Value 的状态是还未初始化,因此在执行 console.log(mod1Value) 时,代码会抛出没有初始化的错误。如果将 mod1.mjs 中的 let mod1Value 改为 var mod1Value,由于 var 天然有变量提升的特性,会先初始化为 undefined,实际运行时不会报错,会输出 undefined。


我们可以看出 ESModule import 不是简单的类似 require 的同步加载机制,下面我们来分析一下相比于同步的加载方式,ESModule 的加载策略上有哪些优势。


ESModule 加载运行策略相比于同步的方式又哪些优势


 能够用更快的速度并发加载代码资源


在 Web 领域,网络的加载耗时一直是用户体验非常重要的影响因子,在 ESModule 的策略下,实际模块的依赖的解析不需要依赖代码的执行,而是直接通过静态分析的方式进行,这使得浏览器、Node 等应用可以用尽可能快的速度完成依赖的收集和资源的请求,而不会受具体模块代码执行耗时以及前后顺序的影响,可以使用尽可能多的并发请求来快速完成加载。


同时从最开始的例子中可以看出,在 ESModule 中 import 在文件的中的位置不会影响具体的行为表现,这使得浏览器可以进行类似 HTML 流式渲染一样,对 ESModule 进行 “流式加载”,比如一个 JS 文件有 1000 行,如果第一行写了一个 import,浏览器就可以直接进行对应模块的加载,而无需等待文件加载完成在进行下一个模块的加载。


 支持 TreeShaking 的自动优化

ESModule 在实例化的阶段会完成相关变量的声明和绑定,在这个阶段我们可以得到对应的绑定关系图,比如之前例子中的以下这张图


图片.png


通过这张图我们可以明确的看出 mod2Value 没有被其他模块所引用,从而我们只需要判断在 mod2 内也没有使用 mod2Value ,则 mod2Value 相关的代码是无用的,这也是 TreeShaking 的原理。而在这一阶段,实际的代码还没有被执行,以上的依赖关系完全是按照代码文本的静态分析得出,所以这也保证了,我们在构建时也可以模拟浏览器或Node 进行类似的操作,生成对应的依赖关系图,然后针对单个模块分析哪些方法或变量时没有用,对代码进行自动的删减。


结语


本文简要的介绍了 ESModule 加载和执行的整体过程,在研究的过程也深刻感受到了 ESModule 整体规范的严谨性和完善性,考虑了诸多不同的方面,并不是简单的 CommonJS 的升级版本。对于底层原理更加深入的理解,也能够指导我们在使用一些新的技术能够更有方向性,比如 TreeShaking ,实际上就是充分利用了 ESModule 的规范,实现了非常优雅的代码自动剔除,能够保证几乎 0 成本的代码体积最优。实际上 ESModule 还有很多其他内容,比如 dynamic import、top level await 等,可以值得更多的研究和探索。


最后,本文略去了部分加载和执行过程的细节,对细节感兴趣的同学,推荐一篇博客,https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/


团队介绍


我们是阿里巴巴淘系技术部的创新业务前端部门,负责创新业务团队的业务研发、技术平台建设、以及创新技术探索工作。这里有丰富线下零售门店、供应链体系、线上导购营销系统,随着而来我们需要建设丰富的中后台场景、线下设备场景、线上跨端场景,以及营销导购3D表达力来帮助业务成长。同样的,团队方面我们崇尚用标准化的思维,数据驱动解决问题,用模块化和场景化的思维形成业务解决方案,帮助业务进行提效。

相关文章
|
机器学习/深度学习
罗马数字对照表
罗马数字对照表
729 0
|
前端开发
解决VScode在保存less文件时,自动生成对应的css文件以及安装Easy less之后,计算式子不显示结果的问题
解决VScode在保存less文件时,自动生成对应的css文件以及安装Easy less之后,计算式子不显示结果的问题
|
负载均衡 网络协议 安全
负载均衡4层和7层区别
所谓四层就是基于IP+端口的负载均衡;七层就是基于URL等应用层信息的负载均衡
|
3月前
|
JavaScript 安全 前端开发
如何开发人事及OA管理系统的薪酬管理板块?(附架构图+流程图+代码参考)
本文介绍了如何构建一个高效、合规的企业薪酬管理系统,涵盖薪酬模块的重要性、核心功能、系统架构设计、数据模型、开发实现及安全合规要点。内容包括薪酬配置、数据导入、自动化计算、审批发放、工资条生成与安全分发、报表看板、权限审计等关键环节,并提供详细的业务流程、架构图、核心代码示例及落地开发技巧。适用于HR、财务及技术人员快速搭建薪酬管理系统,提升发薪效率,降低人工错误与合规风险。
|
JSON 资源调度 JavaScript
ES Module使用-原理-包管理工具npm(一)
ES Module使用-原理-包管理工具npm
494 0
ES Module使用-原理-包管理工具npm(一)
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
448 1
|
11月前
|
SQL JavaScript 程序员
数据库LIKE查询屡试不爽?揭秘大多数人都忽视的秘密操作符!
本文分析了因数据库中的不可见空白字符导致的数据查询问题,探讨了问题的成因与特性,并提出了使用 SQL 语句修复问题的有效方案。同时,总结了避免类似问题的经验和注意事项。
169 0
|
前端开发 JavaScript 算法
面试官:【webpack和vite的区别?vite一定比webpack快吗?vite的缺点是什么?webpack的热更新和vite的热更新区别?】
面试官:【webpack和vite的区别?vite一定比webpack快吗?vite的缺点是什么?webpack的热更新和vite的热更新区别?】
4030 1
|
数据可视化 uml
UML图讲解(关联关系,单向关联,双向关联,自关联,组合关系,依赖关系,继承关系,实现关系)
UML图讲解,关联关系,单向关联,双向关联,自关联,组合关系,依赖关系,继承关系,实现关系。
6265 0
UML图讲解(关联关系,单向关联,双向关联,自关联,组合关系,依赖关系,继承关系,实现关系)
|
人工智能 定位技术 图形学
【unity实战】制作敌人的AI,使用有限状态机、继承和抽象类多态 定义不同状态的敌人行为
【unity实战】制作敌人的AI,使用有限状态机、继承和抽象类多态 定义不同状态的敌人行为
686 1