背景
开发过程中,遇到了一个js循环依赖的情况,由于在debug过程中,没有特别关注到引用的逻辑问题,导致该问题花了点时间才发现,故此写文章记录下
如图所示
案例
文件结构
a.js
import { funcB } from './b.js';
export function funcA() {
console.log('funcA calls funcB:');
funcB();
}
b.js
import { funcA } from './a.js';
export function funcB() {
console.log('funcB calls funcA:');
funcA();
}
funcB();
执行
node b.js
执行结果
加载过程详解(步骤顺序):
- Node 开始执行
b.js b.js最顶部是:所以 Node 去加载import { funcA } from './a.js';a.js- 在加载
a.js的过程中,它又写着:又回头 import 了import { funcB } from './b.js';b.js - ⚠️ 这时循环依赖形成了
b.js的执行被暂停,只创建了一个空的 module namespace 对象a.js中能获取的funcB是一个 未初始化的引用,值是undefined
- Node 接着执行
a.js的剩余部分:- 声明
funcA,没问题 - 完成后返回到原来的
b.js加载流程
- 声明
- 回到
b.js:- 声明
funcB - 然后执行:
⬇️funcB(); - 打印
"funcB calls funcA:" - 执行
funcA()
- 声明
- 进入
funcA():- 打印
"funcA calls funcB:" - 尝试调用
funcB()
- 打印
ES Module 的核心机制
这里举个例子
import { funcB } from './b.js';
export function funcA() {
console.log('funcA calls funcB:');
funcB();
}
简洁地说,import时,整个b.js会被加载 & 执行一遍,不管你只导入了一个函数
但 👉 只会执行一次(模块是单例) ,并且:
- 只执行模块顶层代码;
- 不会调用任何导出的函数,除非你主动调用(比如
funcB()); - 只会执行一次,即使多个文件都 import 它(模块缓存机制);
- 只导入你声明的部分(如
funcB),其余导出不进入当前作用域。
模块加载过程(ESM 的标准行为)
- 解析依赖图:分析模块里的
import声明,递归分析依赖 - 模块加载:
- 加载整个依赖模块文件(如
b.js) - 执行它的顶层代码(不是函数体内部代码)
- 构建其导出对象
- 加载整个依赖模块文件(如
- 执行导入模块(如
a.js)的顶层代码
什么是“模块顶层代码”?
模块文件中写在模块最外层、非函数体/类体/回调内部的代码
// example.js
console.log('✅ 顶层代码执行了'); // ✅ 顶层代码
const x = 1; // ✅ 顶层代码
function test() { // ✅ 函数声明也是顶层代码(但内部内容不会执行)
console.log('❌ 函数里的代码不会自动执行');
}
if (x === 1) {
console.log('✅ 条件判断在顶层,也会执行'); // ✅ 顶层条件语句
}
test(); // ✅ 手动调用函数,才会执行函数体里的代码
假设你这样 import:
import { test } from './example.js';
执行过程:
example.js会被 加载一次- 其中的顶层语句如
console.log(...)、const x = 1等 都会立刻执行 - 但
test()函数体内部的console.log('...'),不会执行,除非你手动调用
什么不是“顶层代码”?
| 类型 | 是否是顶层代码 | 是否会自动执行 |
|---|---|---|
| 函数体内部的代码 | ❌ 否 | ❌ 否 |
| 类内部的方法 | ❌ 否 | ❌ 否 |
| 回调函数内部的代码 | ❌ 否 | ❌ 否 |
setTimeout(() => {}) | ❌ 否 | ✅ 被注册但延迟执行 |
if、for 这种控制语句 | ✅ 是 | ✅ 立即执行(只要在顶层) |
总结归纳
| 概念 | 意思 |
|---|---|
| 顶层代码 | 写在模块最外层、非函数体/类体/回调内部的代码 |
| 会自动执行吗 | ✅ 会:常见如变量赋值、console.log、import 等 |
| 不会执行的 | ❌ 不会自动执行函数体、类体、异步回调等 |
| 什么时候执行函数体 | 你自己或其他模块主动调用它时才会执行 |
一句话理解:当你 import 一个模块时,JS 会自动执行它的顶层代码,但不会自动调用函数或方法。你导入的只是它的接口和副作用结果