Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке «циклічна залежність»?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Циклічна залежність** - коли два чи більше модулів (напряму або через ланцюжок) імпортують один одного; Node.js розриває цикл і повертає частково завантажений (незавершений) об'єкт одному з модулів, замість того щоб зациклитися нескінченно. **Ключове:** ES Modules переживають цикли краще за CommonJS, бо їхні імпорти - живі посилання, а не статичні копії значень.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення## 1. Визначення > **Circular dependency (циклічна залежність)** - це ситуація, коли **два (чи більше) модулі залежать один від одного напряму чи опосередковано**, утворюючи **замкнене коло залежностей**. Простіше кажучи: - **Модуль A** імпортує **модуль B** - **Модуль B** імпортує **модуль A** Або ще гірше - через ланцюжок: ```javascript A → B → C → A ``` ## 2. Простий приклад у Node.js ### a.js ```javascript const b = require('./b'); console.log('Модуль A завантажено'); module.exports = 'A'; ``` ### b.js ```javascript const a = require('./a'); console.log('Модуль B завантажено'); module.exports = 'B'; ``` ### main.js ```javascript const a = require('./a'); const b = require('./b'); ``` ### Результат під час запуску: ```javascript Модуль B завантажено Модуль A завантажено ``` Зверни увагу: порядок **не той**, якого ти очікував. ## 3. Що реально відбувається під капотом Коли Node.js зустрічає `require()`, він: 1. Починає завантажувати модуль `a`; 2. Бачить, що `a` вимагає `b`; 3. Починає завантажувати `b`; 4. Але `b` теж вимагає `a`, а `a` **ще не встиг повністю завантажитися**; 5. Node.js повертає **частково завантажений (незавершений)** об'єкт `a` у `b`. Тому за циклічної залежності один із модулів отримає **неповний експорт** (`undefined` чи частковий об'єкт). ## 4. Демонстрація частково завантаженого модуля ### a.js ```javascript exports.loaded = false; const b = require('./b'); console.log('b.loaded =', b.loaded); exports.loaded = true; console.log('a завантажено'); ``` ### b.js ```javascript exports.loaded = false; const a = require('./a'); console.log('a.loaded =', a.loaded); exports.loaded = true; console.log('b завантажено'); ``` ### main.js ```javascript require('./a'); ``` ### Вивід: ```javascript a.loaded = false b завантажено b.loaded = true a завантажено ``` Тобто коли `b` імпортував `a`, він отримав **ще не завершений** об'єкт - `a.loaded` = `false`. ## 5. Чому це відбувається Node.js кешує кожен модуль при `require()`. Коли модуль завантажується, він додається в `require.cache`, навіть якщо не встиг виконатися повністю. Тому за циклічної залежності один із модулів "побачить" **недозавантажену версію** іншого. ## 6. У реальному житті Такі цикли часто виникають **не напряму**, а через ланцюжок імпортів: ```javascript A → utils.js → logger.js → A ``` чи в архітектурі: ```javascript controller → service → model → controller ``` Зазвичай це ознака **поганого проєктування** (надто сильна зв'язаність між модулями). ## 7. Ознаки, що в тебе «circular dependency» - При імпорті отримуєш `undefined`; - Код виконується не в очікуваному порядку; - Під час запуску Node.js виводить попередження: ```javascript Warning: Accessing non-existent property 'X' of module exports inside circular dependency ``` - Поведінка нестабільна чи відрізняється між середовищами. ## 8. Як уникнути циклічних залежностей ### Спосіб 1. Розірвати цикл через третій модуль Створи "спільний" модуль, який зберігає те, що потрібно обом. **Погано:** ```javascript A → B → A ``` **Добре:** ```javascript A → Common B → Common ``` `common.js` ```javascript module.exports = { sharedValue: 42 }; ``` `a.js` ```javascript const common = require('./common'); ``` `b.js` ```javascript const common = require('./common'); ``` ### Спосіб 2. Використовувати відкладене завантаження (`require()` усередині функції) Якщо модуль потрібен **лише під час виклику функції**, можна викликати `require()` пізніше: ```javascript function useB() { const b = require('./b'); // завантажиться лише під час виклику b.doSomething(); } ``` Це розриває "момент циклу" під час старту застосунку. ### Спосіб 3. Змінити архітектуру - Розділи відповідальність між модулями; - Позбався "взаємних" залежностей - нехай один модуль керує іншим; - Використовуй **ін'єкцію залежностей** (dependency injection). ### Спосіб 4. Використовувати ESM (ES Modules) В ES Modules (з `import/export`) циклічні залежності **розв'язуються коректно**, тому що імпорт створює **живі посилання (live bindings)**, а не статичні значення, як у CommonJS. Приклад (працює стабільно): ```javascript // a.mjs import { b } from './b.mjs'; export const a = 'A'; console.log('a бачить b =', b); // b.mjs import { a } from './a.mjs'; export const b = 'B'; console.log('b бачить a =', a); ``` ## 9. Інтуїтивно: чому це проблема Уяви два файли, які запускають один одного одночасно: ```javascript A запускає B → B запускає A → A запускає B → ... ``` Node.js, щоб не піти в нескінченність, "зупиняє" цикл і повертає те, що **встиг завантажити** на момент циклу. Тому ти бачиш "половину об'єкта". ## 10. Інструменти для пошуку циклів Якщо проєкт великий, можна використати утиліти: ```javascript npm install madge -g madge src/ ``` Вона покаже, які файли утворюють циклічні залежності. ## Коротке резюме | Термін | Значення | |---|---| | **Circular dependency** | Коли модулі імпортують один одного (напряму чи через ланцюжок) | | **Причина** | Модуль вимагає інший, який ще не встиг виконатися | | **Наслідок** | Повернення `undefined` чи частково завантаженого модуля | | **Рішення** | Розділення логіки, відкладені імпорти, рефакторинг, ESM | ## Одним реченням > **Циклічна залежність** - це ситуація, коли модулі Node.js імпортують один одного, утворюючи замкнене коло, через що один із них завантажується не повністю, що призводить до помилок і `undefined` при `require()`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.