yes tutorial print modal definicion change javascript module es6-module-loader

javascript - tutorial - ¿Cómo arreglar esta dependencia circular del módulo ES6?



javascript tutorial (4)

Aquí está lo que usé en mi propia biblioteca:

  1. Declare los módulos "internos" que declaran clases sin ninguna dependencia circular.
  2. Declare los módulos públicos que cargan los módulos internos en el orden correcto y agregue cualquier método que necesite hacer referencia a las dependencias circulares.
  3. Haga que el usuario importe cualquiera de los módulos de orientación pública.

interno / a.js

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

interno / b.js

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

interno / c.js

class C { } export {C as default}

c.js

import C from ''./internal/c'' import A from ''./a'' import B from ''./b'' // See http://stackoverflow.com/a/9267343/14731 for why we can''t replace "C.prototype.constructor" let temp = C.prototype; C = function() { // this may run later, after all three modules are evaluated, or // possibly never. console.log(A) console.log(B) } C.prototype = temp; export {C as default}

a.js

import ''./c.js'' import ''./internal/a.js'' export {A as default}

b.js

import ''./c.js'' import ''./internal/b.js'' export {B as default}

Punto de entrada

import A from ''./app/a'' console.log(''Entrypoint'', A)

EDITAR: para más antecedentes, también vea la discusión sobre ES Discutir .

Tengo tres módulos A , B y C A y B importan la exportación predeterminada del módulo C , y el módulo C importa la exportación predeterminada de A y B Sin embargo, el módulo C no depende de los valores importados de A y B durante la evaluación del módulo, solo en tiempo de ejecución después de que los tres módulos hayan sido evaluados. Los módulos A y B dependen del valor importado de C durante su evaluación del módulo.

El código se ve algo como esto:

// --- 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() { // this may run later, after all three modules are evaluated, or // possibly never. console.log(A) console.log(B) } } export {C as default}

Tengo el siguiente punto de entrada:

// --- Entrypoint import A from ''./app/A'' console.log(''Entrypoint'', A)

Pero, lo que realmente sucede es que el módulo B se evalúa primero, y falla con este error en Chrome (usando clases nativas de ES6, no transpilando):

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

Lo que esto significa es que el valor de C en el módulo B cuando se está evaluando el módulo B undefined está undefined porque el módulo C aún no se ha evaluado.

Debería poder reproducirse fácilmente creando esos cuatro archivos y ejecutando el archivo entrypoint.

Mis preguntas son (¿puedo tener dos preguntas concretas?): ¿Por qué es así el orden de carga? ¿Cómo pueden escribirse los módulos dependientes circularmente para que funcionen de modo que el valor de C al evaluar A y B no quede undefined ?

(Creo que el entorno del Módulo ES6 puede ser capaz de descubrir inteligentemente que necesitará ejecutar el cuerpo del módulo C antes de que pueda ejecutar los cuerpos de los módulos A y B ).


Hay otra solución posible ..

// --- Entrypoint import A from ''./app/A'' setTimeout(() => console.log(''Entrypoint'', A), 0)

Sí, es un truco asqueroso pero funciona.


La respuesta es usar "funciones de inicio". Para referencia, vea los dos mensajes que comienzan aquí: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21

La solución se ve así:

// --- 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.

También vea este hilo para obtener información relacionada: https://github.com/meteor/meteor/issues/7621#issuecomment-238992688

Es importante tener en cuenta que las exportaciones son elevadas (puede ser extraño, puede pedir en esdiscus para obtener más información) al igual que var , pero la elevación se produce en todos los módulos. Las clases no se pueden elevar, pero sí las funciones (como en los ámbitos normales pre-ES6, pero a través de los módulos porque las exportaciones son enlaces en vivo que se extienden a otros módulos posiblemente antes de que se evalúen, casi como si hubiera un alcance que abarque todos Módulos donde se puede acceder a los identificadores solo mediante el uso de import ).

En este ejemplo, el punto de entrada importa del módulo A , que importa del módulo C , que importa del módulo B Esto significa que el módulo B se evaluará antes que el módulo C , pero debido al hecho de que la función initC exportada desde el módulo C se initC , al módulo B se le dará una referencia a esta función initC izada, y por lo tanto, el módulo B llama a la llamada initC antes del módulo C es evaluado

Esto hace que la variable var C del módulo C se defina antes de que la class B extends C definición de class B extends C ¡Mágico!

Es importante tener en cuenta que el módulo C debe usar var C , no const ni let , de lo contrario, un error temporal de zona muerta debería, en teoría, lanzarse en un verdadero entorno ES6. Por ejemplo, si el módulo C parecía

// --- 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;` !!

luego, tan pronto como el módulo B llame a initC , se initC un error y la evaluación del módulo fallará.

var se alza dentro del alcance del módulo C , por lo que está disponible para cuando se llama a initC . Este es un gran ejemplo de una razón por la que realmente querría usar var lugar de let o const en un entorno ES6 +.

Sin embargo, puede tomar nota de que el resumen no lo maneja correctamente https://github.com/rollup/rollup/issues/845 , y un hack que parece let C = C se puede usar en algunos entornos como se indica en el Enlace anterior al tema Meteor.

Una última cosa importante a tener en cuenta es la diferencia entre la export default C y la export {C as default} . La primera versión no exporta la variable C del módulo C como un enlace activo, sino por valor. Por lo tanto, cuando se usa la export default C , el valor de var C undefined está undefined y se asignará a una nueva variable var default que se oculta dentro del alcance del módulo ES6, y debido al hecho de que C se asigna a la default (como en var default = C por valor, luego, cuando otro módulo (por ejemplo el módulo B ) acceda a la exportación predeterminada del módulo C el otro módulo accederá al módulo C y accederá al valor de la variable default que siempre estará undefined . Por lo tanto, si el módulo C utiliza export default C , incluso si el módulo B llama a initC (que cambia los valores de la variable C interna del módulo C ), el módulo B no accederá a esa variable C interna, sino que accederá a la variable C interna. variable por default , que aún undefined está undefined .

Sin embargo, cuando el módulo C usa el formulario export {C as default} , el sistema del módulo ES6 usa la variable C como la variable exportada predeterminada en lugar de crear una nueva variable default interna. Esto significa que la variable C es un enlace en vivo. Cada vez que se evalúa un módulo que depende del módulo C , se le asignará la variable C interna del módulo C en ese momento dado, no por valor, sino casi como entregar la variable al otro módulo. Por lo tanto, cuando el módulo B llama a initC , la variable C interna del módulo C se modifica y el módulo B puede usarlo porque tiene una referencia a la misma variable (incluso si el identificador local es diferente). Básicamente, en cualquier momento durante la evaluación del módulo, cuando un módulo utilizará el identificador que importó de otro módulo, el sistema del módulo llega al otro módulo y obtiene el valor en ese momento.

Apuesto a que la mayoría de la gente no sabrá la diferencia entre export default C y export {C as default} , y en muchos casos no lo necesitarán, pero es importante saber la diferencia cuando se usan "enlaces en vivo" en todos los módulos con "Funciones de inicio" para resolver dependencias circulares, entre otras cosas donde los enlaces en vivo pueden ser útiles. No ahondar demasiado en el tema, pero si tiene un singleton, los enlaces vivos se pueden usar como una forma de hacer que el alcance de un módulo sea el objeto singleton, y los enlaces en vivo la forma en que se accede a las cosas desde el singleton.

Una forma de describir lo que está sucediendo con los enlaces en vivo es escribir javascript que se comportaría de manera similar al ejemplo del módulo anterior. Aquí es cómo se verían B módulos B y C de una manera que describe los "enlaces en vivo":

// --- 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()

Esto muestra efectivamente lo que está sucediendo en la versión del módulo ES6: B se evalúa primero, pero la var C y la function initC se function initC través de los módulos, por lo que el módulo B puede llamar a initC y luego usar C inmediato, antes de la var C y la function initC se encuentran en el código evaluado.

Por supuesto, se vuelve más complicado cuando los módulos usan identificadores diferentes, por ejemplo, si el módulo B import Blah from ''./c'' , entonces Blah seguirá siendo un enlace import Blah from ''./c'' a la variable C del módulo C , pero esto no es muy fácil para describir el uso de la elevación normal de variables como en el ejemplo anterior, y de hecho, el https://github.com/rollup/rollup/issues/845 .

Supongamos, por ejemplo, que tenemos el módulo B como el siguiente y los módulos A y C son los mismos:

// --- Module B import Blah, {initC} from ''./c''; initC(); console.log(''Module B'', Blah) class B extends Blah { // ... } export {B as default}

Luego, si usamos JavaScript simple para describir solo lo que sucede con los módulos B y C , el resultado sería así:

// --- 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()

Otra cosa a tener en cuenta es que el módulo C también tiene la initC función initC . Esto es solo en caso de que el módulo C se evalúe primero, no se dañará al inicializarlo.

Y lo último que se debe tener en cuenta es que, en este ejemplo, los módulos A y B dependen de C al momento de la evaluación del módulo , no del tiempo de ejecución. Cuando se evalúan los módulos A y B , se requiere que se defina la exportación de C Sin embargo, cuando se evalúa el módulo C , no depende de que se definan las importaciones A y B El módulo C solo necesitará usar A y B en tiempo de ejecución en el futuro, después de que se evalúen todos los módulos, por ejemplo, cuando el punto de entrada ejecuta un new A() que ejecutará el constructor C Es por esta razón que el módulo C no necesita las funciones initA o initB .

Es posible que más de un módulo en una dependencia circular tenga que depender unos de otros, y en este caso se necesita una solución de "función de inicio" más compleja. Por ejemplo, supongamos que el módulo C quiere console.log(A) durante el tiempo de evaluación del módulo antes de definir la class C :

// --- 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;` !!

Debido al hecho de que el punto de entrada en el ejemplo superior importa A , el módulo C se evaluará antes que el módulo A Esto significa que la declaración de console.log(A) en la parte superior del módulo C se registrará undefined porque la class A aún no se ha definido.

Finalmente, para hacer que el nuevo ejemplo funcione de modo que registre la class A lugar de undefined , todo el ejemplo se vuelve aún más complicado (no he incluido el módulo B y el punto de entrada, ya que no cambian):

// --- 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;` !!

Ahora, si el módulo B quisiera usar A durante el tiempo de evaluación, las cosas se complicarían aún más, pero le dejo esa solución para que lo imagine ...


Recomendaría utilizar la inversión de control. Haga que su constructor de C sea puro agregando un A y un parámetro B como este:

// --- 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) { // this may run later, after all three modules are evaluated, or // possibly never. console.log(A) console.log(B) } } // --- Entrypoint import A from ''./A''; import B from ''./B''; import C from ''./C''; const c = new C(A, B); console.log(''Entrypoint'', C, c); document.getElementById(''out'').textContent = ''Entrypoint '' + C + '' '' + c;

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

Actualización, en respuesta a este comentario: ¿Cómo arreglar esta dependencia circular del módulo ES6?

Alternativamente, si no desea que el consumidor de la biblioteca sepa sobre varias implementaciones, puede exportar otra función / clase que oculte esos detalles:

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

o usa este patrón:

// --- 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); } } // --- Entrypoint import A from "./A"; import B from "./B"; import C from "./C"; const c = new C(); console.log("Entrypoint", C, c); document.getElementById("out").textContent = "Entrypoint " + C + " " + c;

Actualización, en respuesta a este comentario: ¿Cómo arreglar esta dependencia circular del módulo ES6?

Para permitir que el usuario final importe cualquier subconjunto de las clases, simplemente haga un archivo lib.js que exporte la API pública:

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

o:

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

Entonces tú puedes:

// --- Entrypoint import { C } from "./lib"; const c = new C(); const output = ["Entrypoint", C, c]; console.log.apply(console, output); document.getElementById("out").textContent = output.join();