Les dépendances circulaires surviennent lorsqu'un module A requiert un module B, et que ce dernier requiert à son tour le module A. Cette situation peut entraîner des problèmes de chargement récursif, rendant l'exécution du programme impossible si elle n'est pas correctement gérée.
// moduleA.js
const depB = require('moduleB');
// moduleB.js
const depA = require('moduleA');
Éviter les dépendances circulaires est idéal, mais dans les projets de grande envergrue, elles apparaissent fréquemment. Ainsi, les mécanismes de chargement des modules doivent anticiper ce scénario. Les systèmes CommonJS et ES6, deux formats répandus, traitent ce cas différemment.
Fonctionnement du chargement dans CommonJS
CommonJS considère chaque module comme un fichier de script. Lors de la première utilisation de require, le script est exécuté intégralement et un objet est mis en mémoire.
{
id: '...',
exports: { ... },
loaded: true,
// ... autres propriétés
}
L'attribut exports contient les interfaces exportées. Les appels ultérieurs à require pour ce module retournent les valeurs mises en cache, sans réexécution.
Comportement de CommonJS face aux dépendances circulaires
Dans CommonJS, le code est exécuté au moment du chargement. En cas de dépendance circulaire, seule la portion déjà exécutée est disponible via exports.
Exemple avec deux modules interdépendants :
// moduleX.js
exports.etat = 'en cours';
const moduleY = require('./moduleY.js');
console.log('Dans moduleX, moduleY.etat =', moduleY.etat);
exports.etat = 'terminé';
console.log('moduleX terminé');
// moduleY.js
exports.etat = 'en cours';
const moduleX = require('./moduleX.js');
console.log('Dans moduleY, moduleX.etat =', moduleX.etat);
exports.etat = 'terminé';
console.log('moduleY terminé');
Lorsque moduleX charge moduleY, ce dernier tente de charger moduleX. À ce stade, moduleX n'a exécuté qu'une seule ligne, donc moduleX.etat vaut 'en cours' pour moduleY.
Un script principal illustre ce processus :
const x = require('./moduleX.js');
const y = require('./moduleY.js');
console.log('Résultat final : x.etat =', x.etat, ', y.etat =', y.etat);
Exécution :
Dans moduleY, moduleX.etat = en cours
moduleY terminé
Dans moduleX, moduleY.etat = terminé
moduleX terminé
Résultat final : x.etat = terminé , y.etat = terminé
Cela confirme que moduleY accède à la version partielle de moduleX, tandis que moduleX obtient le résultat final de moduleY après son exécution complète.
Mécanisme des modules ES6 avec les dépendances circulaires
Les modules ES6 fonctionnent différemment : l'importation (import) crée une référence dynamique sans exécuter immédiatement le module. Les variables restent liées à leur module source, sans mise en cache des valeurs.
Exemple illustrant la liaison dynamique :
// source.js
export let donnee = 'initiale';
setTimeout(() => donnee = 'modifiée', 500);
// consommateur.js
import {donnee} from './source.js';
console.log(donnee);
setTimeout(() => console.log(donnee), 500);
Résultat :
initiale
modifiée
Les modules ES6 ne se soucient pas des cycles de dépendances ; ils créent simplement des liens de référence. La responsabilité de la cohérence des valeurs incombe au développeur.
Considérons un exemple où deux modules s'appellent mutuellement :
// alpha.js
import {beta} from './beta.js';
export function alpha() {
beta();
console.log('alpha exécuté');
}
alpha();
// beta.js
import {alpha} from './alpha.js';
export function beta() {
if (Math.random() > 0.5) {
alpha();
}
}
Contrairement à CommonJS, ce code s'exécute sans erreur dans un environnement ES6, car les références sont résolues dynamiquement lors de l'appel effectif.
Un autre exemple avec un compteur partagé :
// pair.js
import { impair } from './impair.js';
export let compteur = 0;
export function pair(n) {
compteur++;
return n === 0 || impair(n - 1);
}
// impair.js
import { pair } from './pair.js';
export function impair(n) {
return n !== 0 && pair(n - 1);
}
L'exécution de pair(10) entraîne 6 appels, et pair(20) en entraîne 11 supplémentaires, totalisant 17 pour le compteur.
En CommonJS, ce code échouerait lors de l'initialisation, car pair ne serait pas encore défini au moment où impair le requiert.
Dépendances circulaires dans les applications Vue.js
Dans les frameworks comme Vue.js, les dépendances circuliares entre composants suivent les mêmes principes que les modules JavaScript. Trois solutions courantes existent :
- Importer un composant avant sa création dans un autre composant.
- Utiliser des imports dynamiques pour charger les modules à l'exécution.
- Enregistrer les composants globalement pour éviter les imports explicites.