← Назад к вопросам

Как браузер решает циклические зависимости?

2.0 Middle🔥 171 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Циклические зависимости и модули

Циклические зависимости возникают, когда модуль A импортирует модуль B, а модуль B импортирует модуль A. Браузер и bundlers обрабатывают это несколькими способами.

Что такое циклическая зависимость?

// moduleA.js
import { funcB } from './moduleB';

export function funcA() {
  return funcB() + 1;
}

// moduleB.js
import { funcA } from './moduleA';

export function funcB() {
  return funcA() - 1;
}

// Проблема: A зависит от B, B зависит от A
// Это циклическая зависимость

Как JavaScript разрешает циклические зависимости

ES6 модули (современный стандарт)

JavaScript использует lazy evaluation для циклических зависимостей:

// userModule.js
let postModule;

export function getUser(id) {
  if (!postModule) {
    postModule = require('./postModule');
  }
  return {
    id,
    latestPost: postModule.getLatestPost(id)
  };
}

// postModule.js
let userModule;

export function getLatestPost(userId) {
  if (!userModule) {
    userModule = require('./userModule');
  }
  const user = userModule.getUser(userId);
  return { author: user.name, text: 'Latest post' };
}

Этап 1: Инициализация модуля

Browser загружает moduleA
  -> moduleA импортирует moduleB
    -> moduleB импортирует moduleA (уже загружается)
      -> JavaScript возвращает ПУСТОЙ объект для moduleA
    -> moduleB экспортирует свои функции
  -> moduleA получает объект с funcB
-> moduleA экспортирует свои функции

Этап 2: Использование

// При вызове funcA() во время выполнения:
funcA();
// -> вызывает funcB() (уже загружена и инициализирована)
// -> funcB вызывает funcA() (существует в памяти)

CommonJS vs ES6 Modules

CommonJS (Node.js, старый синтаксис)

// a.js
const b = require('./b');

function funcA() {
  return b.funcB() + 1;
}

module.exports = { funcA };

// b.js
const a = require('./a');

function funcB() {
  return a.funcA() - 1;  // Может быть undefined!
}

module.exports = { funcB };

Проблема: CommonJS экспортирует объект сразу же, что может привести к undefined:

// Выполнение:
// require('a') -> a загружается
//   require('b') в a -> b загружается
//     require('a') в b -> возвращает a (не полностью инициализирована)
//       a.funcA = undefined // Ещё не экспортирована

Решение для CommonJS:

// a.js
function funcA() {
  const b = require('./b'); // Ленивый import
  return b.funcB() + 1;
}

module.exports = { funcA };

// b.js
function funcB() {
  const a = require('./a'); // Ленивый import
  return a.funcA() - 1;
}

module.exports = { funcB };

Практический пример: Компоненты React

// Header.tsx
import { Navigation } from './Navigation';

export function Header() {
  return (
    <header>
      <Navigation />
    </header>
  );
}

// Navigation.tsx
import { Header } from './Header';

export function Navigation() {
  return (
    <nav>
      {/* Использует Header где-то */}
    </nav>
  );
}

// Это циклическая зависимость!

Решение 1: Извлечь общий компонент

// Layout.tsx (родитель)
import { Header } from './Header';
import { Navigation } from './Navigation';

export function Layout() {
  return (
    <>
      <Header />
      <Navigation />
    </>
  );
}

// Header.tsx (не импортирует Navigation)
export function Header() {
  return <header>Logo</header>;
}

// Navigation.tsx (не импортирует Header)
export function Navigation() {
  return <nav>Menu</nav>;
}

Решение 2: Ленивый импорт

// Navigation.tsx
import { lazy, Suspense } from 'react';

const Header = lazy(() => import('./Header'));

export function Navigation() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Header />
    </Suspense>
  );
}

Как bundler разрешает циклические зависимости

Webpack, Rollup, Vite

Bundlers берут на себя управление циклическими зависимостями:

// Webpack создаёт namespace объект для модулей
var modules = {
  'moduleA': function(exports, require) {
    var moduleB = require('moduleB');
    // ...
  },
  'moduleB': function(exports, require) {
    var moduleA = require('moduleA');
    // ...
  }
};

// Благодаря инкапсуляции в функции, оба модуля
// могут ссылаться друг на друга в runtime

Когда возникают проблемы

1. Undefined во время инициализации

// counter.js
export let count = 0;

import { increment } from './incrementer';

export function getCount() {
  return count;
}

// incrementer.js
import { getCount } from './counter';

export function increment() {
  count++;
}

// Проблема: при загрузке counter.js
// increment ещё не определена

2. Бесконечная рекурсия

// moduleA.js
from moduleB import funcB

function funcA():
    funcB()  // Вызывает B

// moduleB.js
from moduleA import funcA

function funcB():
    funcA()  # Вызывает A -> вызывает B -> вызывает A...

Best Practices

1. Избегай циклических зависимостей

  • Рефакторь код так, чтобы не было циклических ссылок
  • Извлекай общую логику в третий модуль
// utils.js - без зависимостей
export function sharedLogic() { }

// moduleA.js
import { sharedLogic } from './utils';

// moduleB.js
import { sharedLogic } from './utils';
// moduleA и B не зависят друг от друга

2. Используй ленивый импорт для вынужденных случаев

export function funcA() {
  const { funcB } = require('./b');  // Динамический импорт
  return funcB();
}

3. Используй dependency injection

// moduleA.js
export function funcA(funcB) {
  return funcB();
}

// moduleB.js
export function funcB() { }

// index.js
import { funcA } from './a';
import { funcB } from './b';

const result = funcA(funcB);

4. Проверяй код при сборке

# Webpack может предупредить о циклических зависимостях
webpack --mode development

# ESLint плагин
# eslint-plugin-import может выявить циклы

Инструменты для диагностики

// Найти циклические зависимости
// npm install --save-dev circular-dependency-plugin

// webpack.config.js
const CircularDependencyPlugin = require('circular-dependency-plugin');

module.exports = {
  plugins: [
    new CircularDependencyPlugin({
      exclude: /a\.js|node_modules/,
      include: /dir/,
      failOnError: true
    })
  ]
};

Заключение

  • Современные ES6 модули обрабатывают циклические зависимости через lazy evaluation
  • Bundlers дополнительно оборачивают модули в функции для безопасности
  • Лучший подход - избегать циклических зависимостей через правильную архитектуру
  • Если они неизбежны - используй ленивый импорт или dependency injection