47 votes

Comment résoudre cette dépendance circulaire des modules ES6 ?

EDIT : Pour plus de contexte, voir également la discussion sur ES Discuss.


J'ai trois modules A, B et C. Les modules A et B importent l'export par défaut du module C, et le module C importe l'export par défaut à la fois de A et de B. Cependant, le module C ne dépend pas des valeurs importées de A et de B lors de l'évaluation du module, seulement à un moment donné lors de l'exécution après que les trois modules aient été évalués. Les modules A et B dépendent de la valeur importée de C lors de leur évaluation de module.

Le code ressemble à ceci :

// --- Module A

import C from 'C'
class A extends C {
// ...
}
export {A as default}

.

// --- Module B

import C from 'C'
class B extends C {
// ...
}
export {B as default}

.

// --- Module C

import A from 'A'
import B from 'B'
class C {
constructor() {
// cela peut s'exécuter plus tard, après que les trois modules aient été évalués, ou
// peut-être jamais.
console.log(A)
console.log(B)
}
}
export {C as default}

J'ai le point d'entrée suivant :

// --- Point d'entrée

import A from './app/A'
console.log('Point d'entrée', A)

Cependant, ce qui se passe réellement est que le module B est évalué en premier, et il échoue avec cette erreur dans Chrome (en utilisant des classes ES6 natives, sans transpilage) :

Uncaught TypeError : Class extends value undefined is not a function or null

Cela signifie que la valeur de C dans le module B lors de l'évaluation du module B est undefined car le module C n'a pas encore été évalué.

Vous devriez pouvoir facilement reproduire en créant ces quatre fichiers et en exécutant le fichier d'entrée.

Mes questions sont (puis-je poser deux questions concrètes ?) : Pourquoi l'ordre de chargement est-il ainsi ? Comment les modules à dépendance circulaire peuvent-ils être écrits de manière à ce que le valeur de C lors de l'évaluation de A et de B ne soit pas undefined ?

(Je penserais que l'environnement des modules ES6 pourrait intelligemment découvrir qu'il devra exécuter le corps du module C avant de pouvoir éventuellement exécuter les corps des modules A et B).

4 votes

Ah, j'ai voulu cette question en tant que question canonique depuis longtemps, voyons quand j'aurai le temps de répondre à tout.

0 votes

Joe, je vois que tu as posté une solution sur esdiscuss.org/topic/… mais je ne comprends pas à quoi font référence CircularDep et NonCircularDep. Pour moi, tous les modules dans la question contiennent une forme de dépendances circulaires. Pourrais-tu s'il te plaît poster une réponse en termes de A, B, C tels que définis dans cette question?

0 votes

@Gili Hey, si tu peux répondre dans ce fil de discussion, ce serait génial. Je pense que pour ce faire, il te suffit d'envoyer un e-mail avec le même sujet.

27voto

trusktr Points 4518

La réponse est d'utiliser les "fonctions init". Pour référence, regardez les deux messages qui commencent ici : https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21

La solution ressemble à ceci :

// --- Module A

import C, {initC} from './c';

initC();

console.log('Module A', C)

class A extends C {
    // ...
}

export {A as default}

-

// --- Module B

import C, {initC} from './c';

initC();

console.log('Module B', C)

class B extends C {
    // ...
}

export {B as default}

-

// --- Module C

import A from './a'
import B from './b'

var C;

export function initC(){
    if (C) return;

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

-

// --- Entrypoint

import A from './A'
console.log('Entrypoint', new A) // runs the console.logs in the C
constructor.

Voir également ce fil de discussion pour des informations connexes : https://github.com/meteor/meteor/issues/7621#issuecomment-238992688

Il est important de noter que les exportations sont hissées (cela peut être étrange, vous pouvez demander dans esdiscuss pour en savoir plus) tout comme var mais le levage se fait à travers les modules. Les classes ne peuvent pas être hissées, mais les fonctions peuvent l'être (tout comme dans les scopes normaux pré-ES6, mais à travers les modules parce que les exportations sont des liaisons vivantes qui atteignent d'autres modules éventuellement avant d'être évaluées, presque comme s'il y avait un scope qui englobe tous les modules où les identifiants ne peuvent être accédés que par l'utilisation de import ).

Dans cet exemple, le point d'entrée importe du module A qui importe du module C qui importe du module B . Cela signifie que le module B sera évalué avant le module C mais en raison du fait que l'exporté initC fonction du module C est hissé, le module B recevra une référence à ce houblon. initC et donc le module B appel appel initC avant le module C est évaluée.

Cela entraîne le var C variable du module C pour être défini avant le class B extends C définition. Magique !

Il est important de noter que le module C doit utiliser var C pas const ou let sinon une erreur de zone morte temporelle devrait théoriquement être déclenchée dans un véritable environnement ES6. Par exemple, si le module C ressemble à

// --- Module C

import A from './a'
import B from './b'

let C;

export function initC(){
    if (C) return;

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

puis dès que le module B appelle initC une erreur sera déclenchée et l'évaluation du module échouera.

var est hissé dans le cadre du module C Il est donc disponible lorsque initC s'appelle. C'est un excellent exemple d'une raison pour laquelle vous voudriez vraiment utiliser var au lieu de let ou const dans un environnement ES6+.

Cependant, vous pouvez noter que le rollup ne gère pas cela correctement. https://github.com/rollup/rollup/issues/845 et un hack qui ressemble à let C = C peut être utilisé dans certains environnements, comme indiqué dans le lien ci-dessus vers le problème de Meteor.

Une dernière chose importante à noter est la différence entre export default C et export {C as default} . La première version n'est pas exporter le C variable du module C comme une liaison vivante, mais par valeur. Ainsi, lorsque export default C est utilisé, la valeur de var C est undefined et sera assigné à une nouvelle variable var default qui est caché à l'intérieur de la portée du module ES6, et en raison du fait que C est assigné sur default (dans le mot anglais var default = C par valeur, alors chaque fois que l'exportation par défaut du module C est accessible par un autre module (par exemple le module B ), l'autre module s'insère dans le module C et en accédant à la valeur de la default qui sera toujours undefined . Ainsi, si le module C utilise export default C alors même si le module B appelle initC (qui fait modifier les valeurs du module C interne de l'entreprise C variable), module B n'accèdera pas réellement à ce système interne C il accédera à la variable default variable, qui est toujours undefined .

Cependant, lorsque le module C utilise la forme export {C as default} le système de modules ES6 utilise l'option C comme variable exportée par défaut plutôt que de créer une nouvelle variable interne. default variable. Cela signifie que la C est une liaison vivante. Chaque fois qu'un module dépendant d'un module C est évalué, il recevra le module C interne de l'entreprise C à ce moment précis, non pas par valeur, mais presque comme si on transmettait la variable à l'autre module. Ainsi, lorsque le module B appelle initC , module C interne de l'entreprise C est modifiée, et le module B est capable de l'utiliser car il a une référence à la même variable (même si l'identifiant local est différent) ! En fait, à tout moment au cours de l'évaluation du module, lorsqu'un module utilise l'identifiant qu'il a importé d'un autre module, le système du module accède à l'autre module et obtient la valeur à ce moment précis.

Je parie que la plupart des gens ne connaissent pas la différence entre export default C et export {C as default} Dans la plupart des cas, ils n'en auront pas besoin, mais il est important de connaître la différence lorsqu'on utilise des "live bindings" sur des modules avec des "init functions" afin de résoudre les dépendances circulaires, entre autres choses où les live bindings peuvent être utiles. Sans vouloir m'éloigner du sujet, si vous avez un singleton, les liaisons vivantes peuvent être utilisées comme un moyen de faire en sorte que la portée d'un module soit l'objet singleton, et les liaisons vivantes la façon dont les choses du singleton sont accédées.

Une façon de décrire ce qui se passe avec les liaisons en direct est d'écrire du javascript qui se comporterait comme l'exemple de module ci-dessus. Voici ce que les modules B et C pourrait ressembler d'une manière qui décrit les "liens vivants" :

// --- Module B

initC()

console.log('Module B', C)

class B extends C {
    // ...
}

// --- Module C

var C

function initC() {
    if (C) return

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC()

Cela montre efficacement ce qui se passe dans la version du module ES6 : B est évalué en premier, mais var C et function initC sont hissés à travers les modules, de sorte que le module B est capable d'appeler initC et ensuite utiliser C tout de suite, avant var C et function initC sont rencontrés dans le code évalué.

Bien sûr, cela devient plus compliqué lorsque les modules utilisent des identifiants différents, par exemple si le module B a import Blah from './c' alors Blah sera toujours une liaison vivante avec le C variable du module C mais cela n'est pas très facile à décrire en utilisant un levage variable normal comme dans l'exemple précédent, et en fait Rollup ne le gère pas toujours correctement .

Supposons par exemple que nous ayons un module B comme les suivants et les modules A et C sont les mêmes :

// --- Module B

import Blah, {initC} from './c';

initC();

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

export {B as default}

Si nous utilisons du JavaScript pour décrire uniquement ce qui se passe avec les modules B et C le résultat serait le suivant :

// --- Module B

initC()

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

// --- Module C

var C
var Blah // needs to be added

function initC() {
    if (C) return

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
    Blah = C // needs to be added
}

initC()

Une autre chose à noter est que le module C a également le initC appel de fonction. C'est juste au cas où le module C est toujours évaluée en premier, il n'y a pas de mal à l'initialiser à ce moment-là.

Et la dernière chose à noter est que dans ces exemples, les modules A et B dépendent de C au moment de l'évaluation du module mais pas au moment de l'exécution. Lorsque les modules A et B sont évalués, puis exigent pour le C l'exportation à définir. Cependant, lorsque le module C est évaluée, elle ne dépend pas de A et B les importations étant définies. Module C n'auront besoin que d'utiliser A et B au moment de l'exécution dans le futur, après que tous les modules aient été évalués, par exemple lorsque le point d'entrée s'exécute new A() qui exécutera le C constructeur. C'est pour cette raison que le module C n'a pas besoin initA ou initB fonctions.

Il est possible que plusieurs modules dans une dépendance circulaire doivent dépendre les uns des autres, et dans ce cas, une solution plus complexe de "fonction d'initialisation" est nécessaire. Par exemple, supposons que le module C veut console.log(A) pendant le temps d'évaluation du module avant class C est défini :

// --- Module C

import A from './a'
import B from './b'

var C;

console.log(A)

export function initC(){
    if (C) return;

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

En raison du fait que le point d'entrée dans l'exemple du haut importe A le C sera évalué avant le module A module. Cela signifie que console.log(A) en haut du module C enregistrera undefined parce que class A n'a pas encore été défini.

Enfin, pour faire fonctionner le nouvel exemple de manière à ce qu'il enregistre class A au lieu de undefined Si le module B est modifié, l'exemple entier devient encore plus compliqué (j'ai laissé de côté le module B et le point d'entrée, car ils ne changent pas) :

// --- Module A

import C, {initC} from './c';

initC();

console.log('Module A', C)

var A

export function initA() {
    if (A) return

    initC()

    A = class A extends C {
        // ...
    }
}

initA()

export {A as default} // IMPORTANT: not `export default A;` !!

-

// --- Module C

import A, {initA} from './a'
import B from './b'

initA()

var C;

console.log(A) // class A, not undefined!

export function initC(){
    if (C) return;

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

Maintenant, si le module B voulait utiliser A pendant le temps d'évaluation, les choses deviendraient encore plus compliquées, mais je vous laisse imaginer cette solution...

5 votes

Homme, c'est tellement confus. Quelle est la différence entre la dépendance circulaire étant visible au moment de l'évaluation du module par rapport à l'exécution ? Autrement dit, quel est l'avantage pratique de cette approche ?

0 votes

Eh bien, si vous voulez exporter la classe A étend C, alors C doit simplement être évalué lorsque la classe A est définie, car les classes ne peuvent pas étendre non défini. Essayez d'exécuter la classe A étend non défini {} dans votre console.

0 votes

La dépendance C est nécessaire lorsque le module est évalué, sinon A va étendre undefined et une erreur sera lancée. Les dépendances au moment de l'exécution signifient que la dépendance n'est pas nécessaire jusqu'à un certain moment dans le futur, par exemple, lorsque l'utilisateur du module A appelle new A à un moment donné dans le futur, ou peut-être jamais. Si l'utilisateur n'appelle jamais new A, alors les instructions console.log ne s'exécuteront jamais. Ainsi, les dépendances au moment de l'exécution sont des dépendances qui sont utilisées à un moment donné après l'évaluation des modules et éventuellement elles ne seront jamais utilisées. Vous comprenez ce que je veux dire?

5voto

user1925631 Points 1

Je recommanderais d'utiliser l'inversion de contrôle. Rendez votre constructeur C pur en ajoutant des paramètres A et B comme ceci:

// --- Module A

import C from './C';

export default class A extends C {
    // ...
}

// --- Module B

import C from './C'

export default class B extends C {
    // ...
}

// --- Module C

export default class C {
    constructor(A, B) {
        // cela peut s'exécuter plus tard, après l'évaluation des trois modules, ou
        // éventuellement jamais.
        console.log(A)
        console.log(B)
    }
}

// --- Point d'entrée

import A from './A';
import B from './B';
import C from './C';
const c = new C(A, B);
console.log('Point d\'entrée', C, c);
document.getElementById('out').textContent = 'Point d\'entrée ' + C + ' ' + c;

https://www.webpackbin.com/bins/-KlDeP9Rb60MehsCMa8u

Mise à jour, en réponse à ce commentaire: Comment résoudre cette dépendance circulaire de module ES6 ?

Alternativement, si vous ne voulez pas que le consommateur de la bibliothèque connaisse les différentes implémentations, vous pouvez soit exporter une autre fonction/classe qui cache ces détails:

// Module ConcreteCImplementation
import A from './A';
import B from './B';
import C from './C';
export default function () { return new C(A, B); }

ou utilisez ce pattern:

// --- Module A

import C, { registerA } from "./C";

export default class A extends C {
  // ...
}

registerA(A);

// --- Module B

import C, { registerB } from "./C";

export default class B extends C {
  // ...
}

registerB(B);

// --- Module C

let A, B;

const inheritors = [];

export const registerInheritor = inheritor => inheritors.push(inheritor);

export const registerA = inheritor => {
  registerInheritor(inheritor);
  A = inheritor;
};

export const registerB = inheritor => {
  registerInheritor(inheritor);
  B = inheritor;
};

export default class C {
  constructor() {
    // this may run later, after all three modules are evaluated, or
    // possibly never.
    console.log(A);
    console.log(B);
    console.log(inheritors);
  }
}

// --- Point d'entrée

import A from "./A";
import B from "./B";
import C from "./C";
const c = new C();
console.log("Point d'entrée", C, c);
document.getElementById("out").textContent = "Point d'entrée " + C + " " + c;

Mise à jour, en réponse à ce commentaire: Comment résoudre cette dépendance circulaire de module ES6 ?

Pour permettre à l'utilisateur final d'importer n'importe quel sous-ensemble des classes, créez simplement un fichier lib.js exportant l'API publique:

import A from "./A";
import B from "./B";
import C from "./C";
export { A, B, C };

ou:

import A from "./A";
import B from "./B";
import C from "./ConcreteCImplementation";
export { A, B, C };

Ensuite, vous pouvez:

// --- Point d'entrée

import { C } from "./lib";
const c = new C();
const output = ["Point d'entrée", C, c];
console.log.apply(console, output);
document.getElementById("out").textContent = output.join();

1 votes

Merci pour la suggestion! Un problème avec cela est que vous avez maintenant déplacé la connaissance de la dépendance de la bibliothèque vers l'utilisateur final, et l'utilisateur final qui pourrait utiliser uniquement C dans ce cas (pour quelque raison que ce soit) devrait connaître A et B, alors qu'auparavant seul l'auteur de la bibliothèque devait le savoir.

0 votes

Génial que vous vous soyez inscrit juste pour répondre à cela. :)

0 votes

Ensuite, que diriez-vous de ceci? webpackbin.com/bins/-Kl_37vgaKD3saNUXqQo

2voto

Jotaf Points 101

Toutes les réponses précédentes sont un peu complexes. Ne devrait-on pas résoudre cela avec des imports "vanilla" ?

Vous pouvez simplement utiliser un index principal unique, à partir duquel tous les symboles sont importés. C'est assez simple pour que JS puisse le comprendre et résoudre l'importation circulaire. Il existe un article de blog vraiment intéressant qui décrit cette solution, mais voici ce que cela donne selon la question de l'auteur :

// --- Module A

import C from './index.js'
...

// --- Module B

import C from './index.js'
...

// --- Module C

import {A, B} from './index.js'
...

// --- index.js
import C from 'C'
import A from 'A'
import B from 'B'
export {A, B, C}

// --- Point d'entrée

import A from './app/index.js'
console.log('Point d'entrée', A)

L'ordre d'évaluation est l'ordre dans index.js (C-A-B). Les références circulaires dans le corps des déclarations peuvent être incluses de cette manière. Ainsi, par exemple, si B et C héritent de A, mais que les méthodes de A contiennent des références à B ou C (ce qui provoquerait une erreur en cas d'importation normale), cela fonctionnera.

1 votes

Oui, JS peut résoudre les importations circulaires de manière native (peu importe s'il y a un module d'index principal ou non). Ce qui importe réellement, c'est l'ordre d'évaluation - pouvez-vous s'il vous plaît ajouter à votre réponse l'explication de la manière dont le module principal résout cela ?

0 votes

Bien sûr, je pensais qu'il était évident que l'ordre d'évaluation est A-B-C.

0 votes

Mais seulement si vous utilisez index.js comme point d'entrée :-) De plus, l'ordre A-B-C est incorrect pour l'OP, qui a besoin que la classe C soit initialisée avant de l'étendre dans A et B.

2voto

Jon Wyatt Points 11

Il existe une autre solution possible..

// --- Point d'entrée

import A from './app/A'
setTimeout(() => console.log('Point d'entrée', A), 0)

Oui c'est un hack dégoûtant mais ça marche

0voto

Mehdi Yeganeh Points 666

Vous pouvez le résoudre avec le chargement dynamique de modules

J'avais le même problème et j'importe simplement des modules de manière dynamique.

Remplacez l'importation à la demande :

import module from 'chemin-du-module';

avec l'importation dynamique :

let module;
import('chemin-du-module').then((res)=>{
    module = res;
});

Dans votre exemple, vous devriez changer c.js comme ceci :

import C from './interne/c'
let A;
let B;
import('./a').then((res)=>{
    A = res;
});
import('./b').then((res)=>{
    B = res;
});

// Voir http://stackoverflow.com/a/9267343/14731 pour la raison pour laquelle nous ne pouvons pas remplacer "C.prototype.constructor"
let temp = C.prototype;
C = function() {
  // ceci peut s'exécuter plus tard, après l'évaluation des trois modules, ou éventuellement jamais.
  console.log(A)
  console.log(B)
}
C.prototype = temp;

export {C as default}

Pour plus d'informations sur l'importation dynamique :

http://2ality.com/2017/01/import-operator.html

Il y a une autre façon expliquée par léo, elle est réservée à ECMAScript 2019:

https://stackoverflow.com/a/40418615/1972338

Pour analyser les dépendances circulaires, Artur Hebda l'explique ici :

https://railsware.com/blog/2018/06/27/how-to-analyze-circular-dependencies-in-es6/

1 votes

C'est très problématique car le code important c.js ne saura pas à quel moment A et B seront disponibles (puisqu'ils sont chargés de manière asynchrone), c'est donc un jeu de roulette russe pour savoir si C va planter ou non.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X