Перейти к содержимому
20 / 33

Как работает циклическая зависимость в модулях?


Полный ответ

Как движок обрабатывает цикл в ESM:

JavaScript
// a.js
import { b } from "./b.js";
console.log("a.js загружен, b =", b);
export const a = "A";

// b.js
import { a } from "./a.js";
console.log("b.js загружен, a =", a);
export const b = "B";

Порядок выполнения (если точка входа — a.js):

  1. Движок начинает парсить a.js, видит import из b.js
  2. Переходит к парсингу b.js
  3. b.js импортирует a из a.js, но a.js ещё не выполнился
  4. a существует как привязка, но ещё не инициализирована
  5. b.js выполняется: console.log("b.js загружен, a =", undefined)
  6. b инициализируется значением "B"
  7. Возврат к a.js: console.log("a.js загружен, b =", "B")
  8. a инициализируется значением "A"

В CommonJS ситуация похожа, но механизм другой:

JavaScript
// a.js (CJS)
const { b } = require("./b");
console.log("a.js загружен, b =", b);
module.exports = { a: "A" };

// b.js (CJS)
const { a } = require("./a");
console.log("b.js загружен, a =", a); // undefined — a.js ещё не экспортировал
module.exports = { b: "B" };

Почему ESM справляется лучше — live bindings:

JavaScript
// counter.js (ESM)
import { increment as incFromB } from "./b-counter.js";

export let count = 0;

export function increment() {
  count++;
}

// b-counter.js (ESM)
import { count } from "./counter.js";

export function increment() {
  // count — живая ссылка, видит актуальное значение
  console.log("b видит count =", count);
}

// Даже при циклических зависимостях live binding обновляется корректно

Реальные кейсы

Как возникает цикл в реальном проекте:

JavaScript
// ❌ Типичная проблема
// user.js
import { Order } from "./order.js";

export class User {
  getOrders() {
    return Order.findByUser(this.id);
  }
}

// order.js
import { User } from "./user.js";

export class Order {
  getUser() {
    return User.findById(this.userId);
  }
}

// User и Order импортируют друг друга — цикл!

Как исправить — вынести общее в третий модуль:

JavaScript
// ✅ Решение 1: третий модуль-посредник
// repository.js
import { User } from "./user.js";
import { Order } from "./order.js";

export function getOrdersForUser(userId) {
  return Order.findByUser(userId);
}

export function getUserForOrder(order) {
  return User.findById(order.userId);
}

// ✅ Решение 2: ленивый импорт
// user.js

export class User {
  async getOrders() {
    const { Order } = await import("./order.js");
    return Order.findByUser(this.id);
  }
}

Резюме

Циклические зависимости технически работают в обеих модульных системах, но часто приводят к неожиданным undefined. ESM справляется лучше благодаря live bindings, но это не повод мириться с циклами. Лучший подход — рефакторинг: вынести общую логику в отдельный модуль или использовать ленивый import().

Как работает циклическая зависимость в модулях? | JScriptiser