JavaScript 模块:理解模块系统

news2025/1/16 18:56:18

前言

现代JavaScript开发毋庸置疑会遇到代码量大和广泛使用第三方库的问题。解决这个问题的方案通常需要把代码拆分成很多部分,然后再通过某种方式将它们连接起来。

在ECMAScript 6模块规范出现之前,虽然浏览器原生不支持模块的行为, 但也迫切需要这样的行为。ECMAScript同样不支持模块,因此希望使用模块模式的库或代码库必须基于JavaScript的语法和词法特性“伪造”出类似模块的行为

因为JavaScript是异步加载的解释型语言,所以得到广泛应用的各种模块实现也表现出不同的形态。这些不同的形态决定了不同的结果,但最终它们都实现了经典的模块模式。

1. 理解模块模式

将代码拆分成独立的块,然后再把这些块连接起来可以通过模块模式来实现。这种模式背后的思想很简单:把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。不同的实现和特性让这些基本的概念变得有点复杂,但这个基本的思想是所有 JavaScript 模块系统的基础。

2. 模块标识符

模块标识符是所有模块系统通用的概念模块系统本质上是键/值实体,其中每个模块都有个可用于引用它的标识符。这个标识符在模拟模块的系统中可能是字符串,在原生实现的模块系统中可能是模块文件的实际路径。

有的模块系统支持明确声明模块的标识,还有的模块系统会隐式地使用文 件名作为模块标识符。不管怎样,完善的模块系统一定不会存在模块标识冲突的问题,且系统中的任何模块都应该能够无歧义地引用其他模块。

将模块标识符解析为实际模块的过程要根据模块系统对标识符的实现。原生浏览器模块标识符必须提供实际JavaScript文件的路径。除了文件路径,Node.js 还会搜索 node_modules 目录,用标识符去匹配包含 index.js 的目录。

3. 模块依赖

模块系统的核心是管理依赖。指定依赖的模块与周围的环境会达成一种契约。本地模块向模块系统声明一组外部模块(依赖),这些外部模块对于当前模块正常运行是必需的。模块系统检视这些依赖,进而保证这些外部模块能够被加载并在本地模块运行时初始化所有依赖。

每个模块都会与某个唯一的标识符关联,该标识符可用于检索模块。这个标识符通常是JavaScript文件的路径,但在某些模块系统中,这个标识符也可以是在模块本身内部声明的命名空间路径字符串。

4. 模块入口

相互依赖的模块必须指定一个模块作为入口(entry point),这也是代码执行的起点。这是理所当然的,因为JavaScript是顺序执行的,并且是单线程的,所以代码必须有执行的起点。入口模块也可能依赖其他模块,其他模块同样可能有自己的依赖。于是模块化JavaScript应用程序的所有模块会构成依赖图。

可以通过有向图来表示应用程序中各模块的依赖关系。下图展示了一个想象中应用程序的模块依赖关系图。

图中的箭头表示依赖方向:模块A依赖模块B和模块C,模块B依赖模块D和模块E,模块C依赖模块E。因为模块必须在依赖加载完成后才能被加载,所以这个应用程序的入口模块A必须在应用程序的其他部分加载后才能执行。

在JavaScript中,“加载”的概念可以有多种实现方式。因为模块是作为包含将立即执行的JavaScript代码的文件实现的,所以一种可能是按照依赖图的要求依次请求各个脚本。对于前面的应用程序来说,下面的脚本请 求顺序能够满足依赖图的要求:

<script src="moduleE.js"></script>
<script src="moduleD.js"></script>
<script src="moduleC.js"></script>
<script src="moduleB.js"></script>
<script src="moduleA.js"></script> 

模块加载是“阻塞的”,这意味着前置操作必须完成才能执行后续操作。每个模块在自己的代码到达浏览器之后完成加载,此时其依赖已经加载并初始化。不过,这个策略存在一些性能和复杂性问题。为一个应用程序而按顺序加载五个JavaScript文件并不理想,并且手动管理正确的加载顺序也颇为棘手。

5. 异步依赖

因为JavaScript可以异步执行,所以如果能按需加载就好了。换句话说, 可以让JavaScript通知模块系统在必要时加载新模块,并在模块加载完成 后提供回调。在代码层面,可以通过下面的伪代码来实现:

// 在模块A里面
load('moduleB').then(function(moduleB) {moduleB.doStuff()
}) 

模块A的代码使用了 moduleB 标识符向模块系统请求加载模块B,并以模块B作为参数调用回调。模块B可能已加载完成,也可能必须重新请求和初始化,但这里的代码并不关心。这些事情都交给了模块加载器去负责。

如果重写前面的应用程序,只使用动态模块加载,那么使用一个 <script> 标签即可完成模块A的加载。模块A会按需请求模块文件,而不会生成必需 的依赖列表。这样有几个好处,其中之一就是性能,因为在页面加载时只需同步加载一个文件

这些脚本也可以分离出来,比如给 <script> 标签应用 defer 或 async 属性,再加上能够识别异步脚本何时加载和初始化的逻辑。此行为将模拟在ES6模块规范中实现的行为。

6. 动态依赖

有些模块系统要求开发者在模块开始列出所有依赖,而有些模块系统则允许开发者在程序结构中动态添加依赖。动态添加的依赖有别于模块开头列出的常规依赖,这些依赖必须在模块执行前加载完毕。

下面是动态依赖加载的例子:

if (loadCondition) {require('./moduleA')
} 

在这个模块中,是否加载moduleA 是运行时确定的。加载moduleA 时可 能是阻塞的,也可能导致执行,且只有模块加载后才会继续。无论怎样, 模块内部的代码在moduleA 加载前都不能执行,因为moduleA 的存在是 后续模块行为正确的关键。

动态依赖可以支持更复杂的依赖关系,但代价是增加了对模块进行静态分析的难度

7. 静态分析

模块中包含的发送到浏览器的JavaScript代码经常会被静态分析,分析工具会检查代码结构并在不实际执行代码的情况下推断其行为。对静态分析友好的模块系统可以让模块打包系统更容易将代码处理为较少的文件。它还将支持在智能编辑器里智能自动完成。

更复杂的模块行为,例如动态依赖,会导致静态分析更困难。不同的模块系统和模块加载器具有不同层次的复杂度。至于模块的依赖,额外的复杂度会导致相关工具更难预测模块在执行时到底需要哪些依赖。

8. 循环依赖

要构建一个没有循环依赖的JavaScript应用程序几乎是不可能的,因此包括CommonJS、AMD和ES6在内的所有模块系统都支持循环依赖。在包含循环依赖的应用程序中,模块加载顺序可能会出人意料。不过,只要恰当地封装模块,使它们没有副作用,加载顺序就应该不会影响应用程序的运行。

在下面的模块代码中(其中使用了模块中立的伪代码),任何模块都可以作为入口模块,即使依赖图中存在循环依赖:

require('./moduleD')
require('./moduleB')

console.log('moduleA')
require('./moduleA')
require('./moduleC')

console.log('moduleB')
require('./moduleB')
require('./moduleD')

console.log('moduleC')
require('./moduleA')
require('./moduleC')

console.log('moduleD') 

修改主模块中用到的模块会改变依赖加载顺序。如果 moduleA 最先加载,则会打印如下输出,这表示模块加载完成时的绝对顺序:

moduleB
moduleC
moduleD
moduleA 

以上模块加载顺序可以用下面的依赖图来表示,其中加载器会执行深度优先的依赖加载:

如果moduleC 最先加载,则会打印如下输出,这表示模块加载的绝对顺序:

moduleD 
moduleA
moduleB
moduleC 

以上模块加载顺序可以通过下面依赖图来表示,其中加载器会执行深度优先的依赖加载:


每文一句:世界上三种东西最宝贵——知识、粮食和友谊。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/145112.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ssh连接ubuntu报错

记录问题&#xff1a;1我在本机windows用ssh rootubuntu连接失败 显示端口21啥的2 打开Ubuntu系统&#xff0c;输入ps -e|grep ssh&#xff0c;发现只有agent&#xff0c;没有server3 安装ssh server&#xff0c;输入sudo apt-get install openssh-server&#xff0c;发现报错信…

仅需一个注解,实现 SpringBoot 项目中的隐私数据脱敏!

这两天在整改等保测出的问题&#xff0c;里面有一个“用户信息泄露”的风险项&#xff08;就是后台系统里用户的一些隐私数据直接明文显示了&#xff09;&#xff0c;其实指的就是要做数据脱敏。数据脱敏&#xff1a;把系统里的一些敏感数据进行加密处理后再返回&#xff0c;达…

一键自动化 | Salesforce发布Automation Anywhere自动化组合!

2022年12月1日&#xff0c;Salesforce推出了一个新的Automation Everywhere Bundle&#xff0c;以加速端到端的工作流编排&#xff08;Workflow Orchestration&#xff09;、跨系统自动化&#xff0c;以及在任何地方嵌入数据和AI驱动的工作流。 该捆绑包完全集成到Salesforce F…

acwing第84场周赛(4788,4789,4890)题解

4788. 最大数量 某商场在一天中一共来了 nn 个客人。 每个客人进入商场的具体时刻&#xff08;精确到分钟&#xff09;已知。 请你计算并输出在同一时刻&#xff08;精确到分钟&#xff09;进入商场的最大客人数量。 输入格式 第一行包含整数 nn。 接下来 nn 行&#xff…

二叉搜索树比起二叉树又有什么不一样呢?

二叉搜索树比起二叉树又有什么不一样呢&#xff1f;&#x1f3d0;什么是二叉搜索树&#x1f3d0;二叉搜索树的实现&#x1f3c0;节点类:&#x1f3c0;构造函数&#x1f3c0;析构函数&#x1f3c0;插入insert⚽非递归版本⚽递归版本&#x1f3c0;查找find⚽非递归版本⚽递归版本…

spring boot 八:SpringBoot响应返回xml数据

spring boot 八&#xff1a;SpringBoot响应返回xml数据 1 前言 根据DispatcherServlet源码分析&#xff0c;研究SpringBoot的Controller返回xml数据的一些方法&#xff0c;包含单独配置和全局配置返回xml数据两种方式。 依赖的SpringBoot版本&#xff1a; <parent>&l…

u盘有病毒怎么办?修复U盘,3个方法解决

U盘和外部的驱动器相比&#xff0c;它的体积更小&#xff0c;携带更加方便&#xff0c;可以轻松地与他人分享文件。虽然U盘使用很方便&#xff0c;但是有时会出现中病毒的情况。u盘有病毒怎么办&#xff1f;如果您也受到此问题的影响&#xff0c;我们可以提供一种有效的方法来修…

物联网架构实例—Ubuntu 安装Redis

1.准备更新apt-get源sudo apt-get update2.安装执行Redis 安装命令sudo apt-get install redis-server3.检查安装状态sudo /etc/init.d/redis-server status查看Redis运行进程ps -aux|grep redis4.将Redis添加到服务器启动项修改/etc/rc.localvim /etc/rc.local将下面的命令加到…

阿里云办公安全产品专家高传贵:零信任,让全球办公安全更简单

2022 年 8 月 30 日&#xff0c;阿里云用户组&#xff08;AUG&#xff09;第 9 期活动在北京举办。活动现场&#xff0c;阿里云办公安全产品专家高传贵&#xff0c;向参会企业代表分享了零信任&#xff0c;让全球办公安全更简单。本文根据演讲内容整理而成。 大家下午好。我今天…

内部类导致的内存泄漏

前两天刷文章偶然翻到一篇因使用非静态内部类时导致内存泄漏的问题,出于好奇自己也动手一试 什么叫内存泄漏 内存泄漏&#xff08;Memory Leak&#xff09;是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放&#xff0c;造成系统内存的浪费&#xff0c;导致程序…

WuThreat首个发布全球领先的身份安全云产品ITDR Cloud

随着数字化、人工智能&#xff0c;公有/私有云&#xff0c;物联网络及5G等技术的全面普及和迭代更新&#xff0c;身份管理建设作为企业重要的基础设施。然而现在黑客攻击手段复杂多样&#xff0c;在历年的实战攻防演习中有大量的应用系统与基础设施的的身份入口被攻破&#xff…

【从零开始学习深度学习】38. Pytorch实战案例:梯度下降、随机梯度下降、小批量随机梯度下降3种优化算法对比【含数据集与源码】

本文将使用一个来自NASA测试不同飞机机翼噪音的数据集&#xff0c;通过梯度下降、随机梯度下降、小批量随机梯度下降这3种优化算法进行模型训练&#xff0c;比较3种训练结果的差异。 目录1. 梯度下降、随机梯度下降、小批量随机梯度下降区别2. 读取训练数据3. 从零实现3种梯度算…

多线程与高并发(16)——线程池原理(ThreadPoolExecutor源码)

本文从ThreadPoolExecutor源码来理解线程池原理。 ThreadPoolExecutor使用了AQS、位操作、CAS操作等。在看这篇文章之前&#xff0c;需要具备以下知识&#xff1a; 多线程与高并发&#xff08;6&#xff09;——CAS详解&#xff08;包含ABA问题&#xff09; 多线程与高并发&…

腾讯三面:进程写文件过程中,进程崩溃了,文件数据会丢吗?

进程写文件&#xff08;使用缓冲 IO&#xff09;过程中&#xff0c;写一半的时候&#xff0c;进程发生了崩溃&#xff0c;会丢失数据吗&#xff1f; 答案&#xff0c;是不会的。 因为进程在执行 write &#xff08;使用缓冲 IO&#xff09;系统调用的时候&#xff0c;实际上是…

企业宣传片制作配音,我们该从哪里找?

优秀的品质的配音是制作优质企业视频必不可少的硬件条件。因此&#xff0c;许多公司视频配音或旁白声音是由专门从事配音行业的人员配音的。 首先是在宣传视频中配音的作用 1.宣传视频的配音为您建立企业形象 2.宣传视频的配音将为您打开市场 3.宣传视频的配音将使您的宣传…

深入理解Synchronized

Synchronized 底层原理 Synchronized的语义底层是通过一个 Monitor 的对象来完成&#xff0c;其实wait/notify等方法也依赖于 Monitor 对象&#xff0c;这就是为什么只有在同步的块中&#xff0c;拿到锁之后&#xff0c;才能调用wait/notify等方法&#xff0c;否则会抛出java.…

AI助力产品质量检验,基于YOLO实现瓷砖缺陷问题检测识别

在我之前的文章中也写过很多关于生产质检相关的实践文章&#xff0c;一直觉得这块是比较有意思的应用方向&#xff0c;做出来的模型能够以一种更加直观贴切的形式展现出来&#xff0c;瓷砖缺陷问题检测识别也是一个比较老的话题了&#xff0c;今天还是想拿出来具体实践做一下&a…

Golang.org/x库初探1——image库

Golang有一个很有意思的官方库&#xff0c;叫golang.org/x&#xff0c;x可能是extends&#xff0c;experimental&#xff0c;总之是一些在官方库中没有&#xff0c;但是又很有用的库。最近花点时间把这里有用的介绍一下。 Image库 提供更多的图像格式 golang.org/x/image库整…

Linux 网络驱动

1. linux 里面驱动三巨头&#xff1a;字符设备驱动、块设备驱动、网络设备驱动。2.嵌入式网络硬件分为两部分&#xff1a; MAC 和 PHY。如果一款芯片数据手册说自己支持网络&#xff0c;一般都是说的这款 SOC 内置 MAC&#xff0c; MAC 类似 I2C 控制器、SPI 控制器一样的外设。…

Java三大技术平台是什么?

为了使软件开发人员、服务提供商和设备生产商可以针对特定的市场进行开发&#xff0c;SUN公司将Java划分为三个技术平台&#xff0c;它们分别是 JavaSE、 JavaEE和 JavaME。Java SE( Java Platform Standard Edition)标准版&#xff0c;是为开发普通桌面和商务应用程序提供的解…