js循环依赖——bug记录

168 阅读3分钟

背景

开发过程中,遇到了一个js循环依赖的情况,由于在debug过程中,没有特别关注到引用的逻辑问题,导致该问题花了点时间才发现,故此写文章记录下

如图所示 image.png

案例

文件结构 image.png

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

执行结果 image.png

加载过程详解(步骤顺序):

  1. Node 开始执行 b.js
  2. b.js 最顶部是:
    import { funcA } from './a.js';
    
    所以 Node 去加载 a.js
  3. 在加载 a.js 的过程中,它又写着:
    import { funcB } from './b.js';
    
    又回头 import 了 b.js
  4. ⚠️ 这时循环依赖形成了
    • b.js 的执行被暂停,只创建了一个空的 module namespace 对象
    • a.js 中能获取的 funcB 是一个 未初始化的引用,值是 undefined
  5. Node 接着执行 a.js 的剩余部分:
    • 声明 funcA,没问题
    • 完成后返回到原来的 b.js 加载流程
  6. 回到 b.js
    • 声明 funcB
    • 然后执行:
      funcB();
      
      ⬇️
    • 打印 "funcB calls funcA:"
    • 执行 funcA()
  7. 进入 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 的标准行为)

  1. 解析依赖图:分析模块里的 import 声明,递归分析依赖
  2. 模块加载:
    • 加载整个依赖模块文件(如 b.js
    • 执行它的顶层代码(不是函数体内部代码)
    • 构建其导出对象
  3. 执行导入模块(如 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(() => {})❌ 否✅ 被注册但延迟执行
iffor 这种控制语句✅ 是✅ 立即执行(只要在顶层)

总结归纳

概念意思
顶层代码写在模块最外层、非函数体/类体/回调内部的代码
会自动执行吗✅ 会:常见如变量赋值、console.log、import 等
不会执行的❌ 不会自动执行函数体、类体、异步回调等
什么时候执行函数体你自己或其他模块主动调用它时才会执行

一句话理解:当你 import 一个模块时,JS 会自动执行它的顶层代码,但不会自动调用函数或方法。你导入的只是它的接口和副作用结果