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):
- Движок начинает парсить
a.js, видитimportизb.js - Переходит к парсингу
b.js b.jsимпортируетaизa.js, ноa.jsещё не выполнилсяaсуществует как привязка, но ещё не инициализированаb.jsвыполняется:console.log("b.js загружен, a =", undefined)bинициализируется значением"B"- Возврат к
a.js:console.log("a.js загружен, b =", "B") 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().