Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >「译」解析 JavaScript 中的循环依赖

「译」解析 JavaScript 中的循环依赖

作者头像
泯泷、
发布于 2025-05-09 04:47:42
发布于 2025-05-09 04:47:42
730
举报
文章被收录于专栏:前端工具前端工具

我写了很多 JavaScript 代码,但循环依赖对我来说一直是个谜。错误信息总是显得随机且难以理解,有时甚至根本没有错误信息!我想更好地理解这个话题,所以进行了一系列实验,并想分享我学到的东西。让我们来解析 JavaScript 中的循环依赖。

什么是循环依赖?

当你的 JavaScript import 语句导致循环时,就会发生循环依赖:

Diagram showing a two-way dependency loop and a three-way dependency loop.
Diagram showing a two-way dependency loop and a three-way dependency loop.

循环可以由两个文件、三个文件或更多文件组成。

每当你的 import 语句创建这样的循环时,你的代码就有可能无法按预期工作。

如何知道何时存在循环依赖?

语言本身没有内置的简单方法!

在 JavaScript 中,循环依赖通常表现为一个看似无关的错误(如 ReferenceErrorTypeError )。这与许多其他编程语言不同,这些语言通常会直接告诉你导入有问题:

  • Python: ImportError
  • Go: import cycle not allowed

那么为什么 JavaScript 不能直接说⚠️ CircularDependencyError 呢?

这是因为 JavaScript 模块设计为按需加载和执行。

当你的浏览器加载一个网页并开始执行第一个 JavaScript 文件时,它并不知道还有多少文件即将到来。这些文件可能还存放在世界另一端的服务器上。

这与 Go 或 Python 程序的情况非常不同,后者的导入系统可以在执行任何一行代码之前分析整个依赖树。

逐步解析 JavaScript 中的循环依赖

解释 JavaScript 给出的错误的最佳方法是逐步解析一个循环依赖的场景:

A diagram showing step-by-step execution of code leading to a circular dependency error.
A diagram showing step-by-step execution of code leading to a circular dependency error.

这是我们在每一步中看到的内容:

步骤 1:在 index.js 的第 1 行,执行暂停以下载 a.js ,以便可以导入其值 a

步骤 2:下载 a.js 后,执行在 a.js 中继续,但在第 1 行暂停以下载 b.js ,以便可以导入其值 b

步骤 3:下载 b.js 后,执行在 b.js 中继续,并在第 1 行找到一个指向 a.js 的导入(循环导入)。

步骤 4: a.js 已经下载,但由于此时我们尚未执行 a.js 中第 1 行之后的内容,因此它没有定义任何导出。因此,我们无法满足 b.js 中的导入。

第 5 步:执行在 b.js 中继续, a 仍未初始化。当在第 3 行调用 a 时,程序报错: ReferenceError: Cannot access 'a' before initialization

总结一下,循环依赖导致代码在未初始化的值下执行。这可能会导致各种错误,比如上面的 ReferenceError

为什么循环依赖有时不会导致错误?

JavaScript 的导入被描述为“实时绑定”。这意味着导入的值可能一开始是未初始化的(由于循环依赖),但在代码的其余部分被评估后变得完全可用。换句话说,一些循环依赖是无错误的,因为它们在你调用受影响的代码之前“自行解决”。

我曾经在一个充满循环导入的代码库中工作,但它们从未引起任何问题。为什么?

这是因为所有代码都定义在函数中,这些函数在所有内容加载完毕之前都不会被调用。

为了演示,我们可以更新最后一个场景,使其以类似的方式工作:

A diagram showing step-by-step execution circular dependency code without any errors.
A diagram showing step-by-step execution circular dependency code without any errors.

步骤 1-4 与上述相同,但从步骤 5 开始有所变化:

步骤 5: a 仍然未初始化,但不是直接调用,而是被放入函数定义中(无错误)。

步骤 6: b.js 完成后, a.js 中的执行继续到第 3 行,该行定义了 a 的导出。从这一点开始,任何调用 a 的代码都将获得一个初始化值,这是由于实时绑定的结果。

步骤 7:我们成功调用了 a() ,它又调用了 b() 。最终,所有代码都被调用且没有错误。

总结来说,当我们实际调用那个“未初始化的 a”时,实时绑定已经更新了它的值,它不再是未初始化的。我们之所以安全,是因为 a 的值只在变量实际使用时才会被获取。

现在,我不会推荐用这种方式来解决依赖问题。我更倾向于彻底移除循环依赖。不过,我打赌有很多生产应用目前依赖于这种行为来处理循环依赖。

防止循环依赖

虽然 JavaScript 可能没有内置的循环依赖检查,但我们仍有办法防止这些问题。

像 madge 和 eslint-plugin-import 这样的第三方工具可以对你的 JavaScript 代码库进行静态分析,并在循环依赖变得难以处理之前检测到它们。一些 monorepo 工具如 NX 和 Rush 在其工作流程中内置了类似的功能。

当然,最好的预防措施是一个组织良好的代码库,具有清晰的共享代码层次结构。

Node / Bun / Webpack / 等呢?

我上面分享的例子主要关注“浏览器中的 ES 模块”这一使用场景,但 JavaScript 运行在许多不同的环境中。服务器端的 JavaScript 不需要通过网络下载其源代码(使其更像 Python),而像 Webpack 这样的打包工具可以将所有代码合并到一个文件中。在这些场景中,循环依赖是否是一个问题?

简而言之,是的。在我的实验中,我惊讶地发现浏览器、服务器和打包器的错误结果基本一致。

例如,使用 Webpack 时, import 语句被移除,但合并后的代码仍然产生相同的错误:

代码语言:js
AI代码解释
复制
// b.js
console.log('b.js:', a); // ReferenceError: Cannot access 'a' before initialization
const b = 'B';

// a.js
console.log('a.js:', b);
const a = 'A';

我还应该提到,虽然 Node.js 在使用 import 语法(ESM)时产生了相同的错误,但在使用 require 语法(CommonJS)时表现不同:

代码语言:js
AI代码解释
复制
$ node node-entry.cjs

(node:13010) Warning: Accessing non-existent property 'Symbol(nodejs.util.inspect.custom)' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:13010) Warning: Accessing non-existent property 'constructor' of module exports inside circular dependency
(node:13010) Warning: Accessing non-existent property 'Symbol(Symbol.toStringTag)' of module exports inside circular dependency

考虑到 CommonJS 是一个完全不同的导入系统,它并不符合 ECMAScript Modules 规范,这就说得通了。将两者进行比较就像是在比较苹果和橘子!

结论

循环依赖可能会让人困惑,但当你逐步分析场景时,它会变得更加清晰。一如既往,没有什么比通过实验来清晰理解这类问题更有效的方法了。

如果你想更详细地查看我的测试结果,请随时查看 repo。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
了解一下ES module 和 Commonjs
最近测试了几个 ES module 和 Commonjs 的例子,理解了之前不太理解的概念,记录一下。要是想多了解的可以去看看阮老师的 Module 那部分。会贴一小部分的代码,不会贴所有验证的代码。
wade
2023/09/01
2630
了解一下ES module 和 Commonjs
YAPI-高效、易用、功能强大的 api 管理平台
在前后端分离的架构流行的时下,前后端部门之间交流更多的就是在数据处理和定义前端的接口,但是一边开发一遍维护接口文档,对于开发者来说是一个非常头疼的事情:
公众号: 云原生生态圈
2020/06/15
2.1K0
YAPI-高效、易用、功能强大的 api 管理平台
浅谈 Node.js 模块机制及常见面试问题解答
Node.js 模块机制采用了 Commonjs 规范,弥补了当前 JavaScript 开发大型应用没有标准的缺陷,类似于 Java 中的类文件,Python 中的 import 机制,Node.js 中可以通过 module.exports、require 来导出和引入一个模块.
用户1462769
2019/11/18
7760
JavaScript之无题之让人烦躁的模块化
  我怎么记得我好像写过相关类型的文章,但是我找遍了我的博客没有~那就再写一遍吧,其实模块化的核心内容也算不上是复杂,只不过需要整理一下,规划一下罢了。嘻嘻。
zaking
2022/10/07
5180
模块化的一些小研究0.前言1.script标签引入2.AMD与CMD3.CommonJS与ES64.循环依赖5.webpack是如何处理模块化的
我们知道最常见的模块化方案有CommonJS、AMD、CMD、ES6,AMD规范一般用于浏览器,异步的,因为模块加载是异步的,js解释是同步的,所以有时候导致依赖还没加载完毕,同步的代码运行结束;CommonJS规范一般用于服务端,同步的,因为在服务器端所有文件都存储在本地的硬盘上,传输速率快而且稳定。
lhyt
2018/10/31
1.3K0
NodeJS学习二CommonJS规范
Node程序由许多个模块组成,每个模块就是一个文件。Node模块采用了CommonJS规范。
空空云
2018/09/27
6130
彻底理清 AMD,CommonJS,CMD,UMD,ES6 modules
1.Rollup 是什么2.CommonJS、AMD、CMD、UMD、ES6 分别的介绍3.ES6 模块与 CommonJS 模块的区别4.模块演进的产物 —— Tree Shaking5.Tree Shaking 应该注意什么
前端迷
2020/02/24
2.5K0
前端科普系列(3):CommonJS 不是前端却革命了前端
上一篇《前端科普系列(2):Node.js 换个角度看世界》,我们聊了 Node.js 相关的东西,Node.js 能在诞生后火到如此一塌糊涂,离不开它成熟的模块化实现,Node.js 的模块化是在 CommonJS 规范的基础上实现的。那 CommonJS 又是什么呢?
2020labs小助手
2020/07/09
1.1K0
【THE LAST TIME】深入浅出 JavaScript 模块化
随着互联网的发展,前端开发也变的越来越复杂,从一开始的表单验证到现在动不动上千上万行代码的项目开发,团队协作就是我们不可避免的工作方式,为了更好地管理功能逻辑,模块化的概念也就渐渐产生了。
Nealyang
2020/01/14
7160
【THE LAST TIME】深入浅出 JavaScript 模块化
聊一聊面试中经常被问到的Tree Shaking
天下武功,唯快不破!最新版的 antd 以及 vue 都对 Tree Shaking 提供了支持。我们内部的组件在支持这部分功能时,也专门梳理了相关的特性。这是四月份写的文章了,长时间不用就会忘,复习一下!
前端迷
2020/08/28
2.1K0
聊一聊面试中经常被问到的Tree Shaking
MacOS入坑指南
注意:从Vmware导入到macos时,需要将整个虚拟镜像的文件夹后缀更改为.vmware格式,即可直接倒入到PD虚拟机
偏有宸机
2020/11/04
8740
MacOS入坑指南
模块化的一些小研究
我们知道最常见的模块化方案有CommonJS、AMD、CMD、ES6,AMD规范一般用于浏览器,异步的,因为模块加载是异步的,js解释是同步的,所以有时候导致依赖还没加载完毕,同步的代码运行结束;CommonJS规范一般用于服务端,同步的,因为在服务器端所有文件都存储在本地的硬盘上,传输速率快而且稳定。
lhyt
2022/09/21
3420
前端基础进阶(十七):详解 ES6 Modules
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
唐志远
2023/08/01
7820
深入分析JavaScript模块循环引用
在线教室中台提供封装了核心能力的教室 SDK,业务方基于教室 SDK 开发面向用户的在线教室 App。最近对教室 SDK 做一次比较大的改动时,我遇到了一个懵逼的问题。这个问题耗费了我 3 天左右时间,让我压力一度大到全身发热。当时虽然解决了问题,但并没有很理解原因。直到一个多月后,才有时间做一些更深入的分析,并写下这篇文章。
玖柒的小窝
2021/10/19
1.9K0
深入分析JavaScript模块循环引用
抖音二面:为什么模块循环依赖不会死循环?CommonJS和ES Module的处理有什么不同?
大家好,我是年年。如果被问到“CommonJS和ES Module的差异”,大概每个前端都都背出几条:一个是导出值的拷贝,一个是导出值的引用;一个是运行时加载,一个是静态编译...
用户9899350
2022/07/29
2.1K0
抖音二面:为什么模块循环依赖不会死循环?CommonJS和ES Module的处理有什么不同?
Vue3CLI(脚手架)
注意:在学习之前需要了解一下node.js和ES6的语法,所以需要本文写了node.js中的npm和ES6的模块化 ---- 一、node.js(npm) node.js:简单的说 Node.js 就是运行在服务端的 JavaScript。Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。 Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。与平时写的js语法基本一样,多
超级小的大杯柠檬水
2023/05/06
3070
Vue3CLI(脚手架)
前端架构师之02_ES6_高级
上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
张哥编程
2024/12/13
1170
前端架构师之02_ES6_高级
聊聊CommonJS与ES6 Module的使用与区别
学了JS并且用过Node.js后,对模块化应该是有所了解和使用了,那么一定见过以下两种模块导入导出的方式
前端迷
2020/11/23
1.5K0
深入Node.js的模块加载机制,手写require函数
模块是Node.js里面一个很基本也很重要的概念,各种原生类库是通过模块提供的,第三方库也是通过模块进行管理和引用的。本文会从基本的模块原理出发,到最后我们会利用这个原理,自己实现一个简单的模块加载机制,即自己实现一个require。
蒋鹏飞
2020/10/15
1.2K0
Webpack入门到精通(AST、Babel、依赖)
从名字上就能看出他们使用的环境了,需要注意的是env,他的作用是将最新js转换为es6代码。预设是babel插件的组合,我们可以看下package.json(截取一部分):
落落落洛克
2021/09/17
6160
Webpack入门到精通(AST、Babel、依赖)
推荐阅读
相关推荐
了解一下ES module 和 Commonjs
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档