如何检测和解决项目中的循环依赖?

检测循环依赖

1. madge 工具

# 安装
npm install -g madge

# 检测循环依赖
madge --circular src/

# 输出示例
# 2 circular dependencies found!
# 1) a.js → b.js → a.js
# 2) c.js → d.js → e.js → c.js

2. Webpack 插件

const CircularDependencyPlugin = require('circular-dependency-plugin')

plugins: [
  new CircularDependencyPlugin({
    exclude: /node_modules/,
    failOnError: false,
    onStart({ compilation }) {
      console.log('start detecting circular dependencies')
    },
    onDetected({ module, paths, compilation }) {
      compilation.warnings.push(new Error(paths.join(' → ')))
    }
  })
]

3. ESLint 规则

// eslint-plugin-import
'import/no-cycle': ['error', { maxDepth: 10 }]

常见循环依赖模式

1. 直接循环

// a.js → b.js → a.js
// a.js
import { b } from './b.js'
// b.js
import { a } from './a.js'

2. 间接循环

// a.js → b.js → c.js → a.js
// a.js
import { b } from './b.js'
// b.js
import { c } from './c.js'
// c.js
import { a } from './a.js'

解决方案

1. 提取共享模块(推荐)

// 原来:A ↔ B
// a.js
import { getData } from './b.js'
export function processA() { return getData() }

// b.js
import { formatA } from './a.js'
export function getData() { return formatA(data) }

// 重构:提取共享部分
// shared.js
export function getData() { return data }
export function formatData(data) { return formatted }

// a.js → 只依赖 shared
// b.js → 只依赖 shared

2. 依赖注入

// a.js
export function createA(b) {
  return {
    process() { return b.getData() }
  }
}

// b.js
export function createB(a) {
  return {
    getData() { return a.format() }
  }
}

// main.js
const a = createA(b)  // 先创建一个引用
const b = createB(a)
a.b = b  // 注入

3. 事件总线

// eventBus.js(无依赖)
export const bus = new EventTarget()

// a.js
import { bus } from './eventBus.js'
bus.addEventListener('needData', (e) => {
  e.detail.callback(getData())
})

// b.js
import { bus } from './eventBus.js'
function getDataFromA() {
  return new Promise(resolve => {
    bus.dispatchEvent(new CustomEvent('needData', {
      detail: { callback: resolve }
    }))
  })
}

4. 延迟导入

// a.js
export function processA() {
  // 运行时导入,避免循环
  const { getData } = require('./b.js')
  return getData()
}

5. 接口/类型提取

// types.js(纯类型,无实现)
export interface IB { getData(): string }

// a.js
import type { IB } from './types.js'
export function processA(b: IB) { return b.getData() }

// b.js
import { processA } from './a.js'
export class BImpl implements IB {
  getData() { return 'data' }
}

最佳实践

  1. 单向依赖:模块应该单向依赖,不互相引用
  2. 分层架构:上层依赖下层,下层不依赖上层
  3. 提取公共模块:循环部分提取到独立模块
  4. CI 检测:在 CI 流程中加入循环依赖检测
  5. Code Review:审查新增依赖关系

同分类其他题目