Подтвердить что ты не робот

Как исправить эту кольцевую зависимость модуля модуля ES6?

EDIT: для получения дополнительной информации см. обсуждение на ES Обсудить.


У меня есть три модуля A, B и C. A и B импортировать экспорт по умолчанию из модуля C, а модуль C импортирует значение по умолчанию из A и B. Однако модуль C не зависит от значений, импортированных из A и B во время оценки модуля, только во время выполнения в какой-то момент после оценки всех трех модулей. Модули A и B действительно зависят от значения, импортированного из C во время их оценки модуля.

Код выглядит примерно так:

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

У меня есть следующая точка входа:

// --- Entrypoint

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

Но на самом деле происходит то, что сначала проверяется модуль B, и он не удается с этой ошибкой в ​​Chrome (используя собственные классы ES6, а не транслировать):

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

Это означает, что значение C в модуле B, когда оценивается модуль B, равно undefined, поскольку модуль C еще не был оценен.

Вы должны иметь возможность легко воспроизводить, создавая эти четыре файла и запуская файл точки входа.

Мои вопросы (могу ли я задать два конкретных вопроса?): Почему порядок загрузки? Как записывать циркулярно-зависимые модули так, чтобы они работали так, чтобы значение C при оценке A и B не было undefined?

(я бы подумал, что среда ES6 Module может разумно обнаружить, что ей нужно будет выполнить тело модуля C, прежде чем он сможет выполнить тела модулей A и B.)

4b9b3361

Ответ 1

Ответ заключается в использовании "функций init". Для справки рассмотрим два сообщения, начинающиеся здесь: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21

Решение выглядит так:

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

Также см. эту тему для соответствующей информации: https://github.com/meteor/meteor/issues/7621#issuecomment-238992688

Важно отметить, что экспорт поднят (может быть странно, вы можете попросить в esdiscuss узнать больше), как и var, но подъем происходит через модули. Классы не могут быть подняты, но функции могут быть (как и в обычных областях до ES6, но в разных модулях, потому что экспорт - это прямые привязки, которые попадают в другие модули, возможно, до их оценки, почти так же, как если бы существовала область, охватывающая все модули, в которых идентификаторы могут быть доступны только с помощью import).

В этом примере импортируется точка входа из модуля A, который импортирует из модуля C, который импортирует из модуля B. Это означает, что модуль B будет оцениваться перед модулем C, но из-за того, что экспортированная функция initC из модуля C будет поднята, модуль B получит ссылку на этот поднятый initC функция и, следовательно, модуль B вызов initC перед тем, как будет оценен модуль C.

Это приводит к тому, что переменная var C модуля C будет определена до определения class B extends C. Магия!

Важно отметить, что модуль C должен использовать var C, а не const или let, в противном случае теоретическая ошибка мертвой зоны должна быть теоретически выбрана в реальной среде ES6. Например, если модуль C выглядел как

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

то как только модуль B вызовет initC, будет выдана ошибка, и оценка модуля завершится неудачно.

var поддерживается в рамках модуля C, поэтому он доступен для вызова initC. Это отличный пример того, почему вы действительно хотите использовать var вместо let или const в среде ES6 +.

Однако вы можете принять всплеск заметок, который не справляется с этим правильно https://github.com/rollup/rollup/issues/845, а взлома, который выглядит как let C = C, может быть используется в некоторых средах, как указано в приведенной выше ссылке на проблему Метеор.

Последнее важное замечание - разница между export default C и export {C as default}. Первая версия не экспортирует переменную C из модуля C в качестве привязки в реальном времени, а по значению. Поэтому, когда используется export default C, значение var C равно undefined и будет назначено на новую переменную var default, которая скрыта внутри области модуля ES6, и из-за того, что назначается C на default (как в var default = C по значению), тогда всякий раз, когда по умолчанию экспортируется модуль C другим модулем (например, модулем B), другой модуль будет входить в модуль C и получать доступ к значение переменной default, которая всегда будет undefined. Поэтому, если модуль C использует export default C, то даже если модуль B вызывает initC (который изменяет значения модуля C внутренняя переменная C), модуль B фактически не будет получать доступ к этой внутренней переменной C, она будет обращаться к переменной default, которая по-прежнему undefined.

Однако, когда модуль C использует форму export {C as default}, система модуля ES6 использует переменную C как экспортированную по умолчанию переменную, а не создает новую внутреннюю переменную default. Это означает, что переменная C является связующим звеном. Каждый раз, когда модуль, зависящий от модуля C, оценивается, ему присваивается переменная C internal C в данный момент, а не по значению, но почти как передача этой переменной другому модулю. Итак, когда модуль B вызывает initC, изменяется модификация модуля C internal C, а модуль B может использовать его, потому что он имеет ссылку на одну и ту же переменную (даже если локальный идентификатор другой)! В принципе, в любое время при оценке модуля, когда модуль будет использовать идентификатор, который он импортировал из другого модуля, система модуля переходит в другой модуль и получает значение в этот момент времени.

Я уверен, что большинство людей не будут знать разницу между export default C и export {C as default}, и во многих случаях им это не понадобится, но важно знать разницу при использовании "живых привязок" по модулю с "функции init" для решения круговых зависимостей, среди прочего, когда живые привязки могут быть полезны. Не для того, чтобы переходить слишком далеко от темы, но если у вас есть синглтон, живые привязки могут быть использованы как способ сделать область видимости модуля единичным объектом, а живые привязки - способом доступа к элементам из singleton.

Один из способов описать, что происходит с живыми привязками, - написать javascript, который будет вести себя аналогично приведенному выше примеру модуля. Здесь какие модули B и C могут выглядеть так, как это описано в "живых привязках":

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

Это эффективно показывает, что происходит в версии модуля ES6: сначала оценивается B, но var C и function initC отображаются на всех модулях, поэтому модуль B может вызывать initC, а затем использовать C, прежде чем var C и function initC встречаются в оцениваемом коде.

Конечно, это усложняется, когда модули используют разные идентификаторы, например, если модуль B имеет import Blah from './c', то Blah по-прежнему будет привязкой к привязке к переменной C модуля C, но это не так просто описать с использованием обычного подъема переменных, как в предыдущем примере, а на самом деле Rollup не всегда правильно его обрабатывает.

Предположим, например, что мы имеем модуль B как следующий, а модули A и C совпадают:

// --- Module B

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

initC();

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

export {B as default}

Тогда, если мы используем простой JavaScript для описания только того, что происходит с модулями B и C, результат будет таким:

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

Еще одно замечание - модуль C также имеет вызов функции initC. Это на всякий случай, когда модуль C всегда оценивается первым, это не повредит для его инициализации.

И последнее, что нужно отметить, это то, что в этом примере модули A и B зависят от C во время оценки модуля, а не во время выполнения. Когда оцениваются модули A и B, тогда для экспорта C необходимо указать экспорт. Однако, когда модуль C оценивается, он не зависит от A и B импорта. Модуль C должен будет использовать A и B во время выполнения в будущем, после того как все модули будут оценены, например, когда точка входа запускает new A(), которая будет запускать конструктор C. По этой причине модуль C не нуждается в функциях initA или initB.

Возможно, что более одного модуля в круговой зависимости должны зависеть друг от друга, и в этом случае требуется более сложное решение "функции init". Например, предположим, что модуль C хочет console.log(A) во время оценки модуля до того, как будет определено 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;` !!

В связи с тем, что точка входа в верхнем примере импортирует A, модуль C будет оцениваться перед модулем A. Это означает, что оператор console.log(A) в верхней части модуля C будет записывать undefined, поскольку class A еще не определен.

Наконец, чтобы новый пример работал так, чтобы он записывал class A вместо undefined, весь пример становится еще более сложным (я оставил модуль B и точку входа, так как они не меняются ):

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

Теперь, если модуль B хотел использовать A во время оценки, все будет еще сложнее, но я оставляю это решение для вас...

Ответ 2

Я бы рекомендовал использовать инверсию управления. Сделайте свой конструктор C чистым, добавив параметр A и B как это:

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

Обновить в ответ на этот комментарий: Как исправить эту кольцевую зависимость модуля ES6?

В качестве альтернативы, если вы не хотите, чтобы потребитель библиотеки знал о различных реализациях, вы можете либо экспортировать другую функцию/класс, который скрывает эти детали:

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

или используйте этот шаблон:

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

Обновить в ответ на этот комментарий: Как исправить эту кольцевую зависимость модуля ES6?

Чтобы позволить конечному пользователю импортировать любое подмножество классов, просто создайте файл lib.js, экспортирующий публикацию, обращенную к api:

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

или:

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

Затем вы можете:

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

Ответ 3

Существует еще одно возможное решение.

// --- Entrypoint

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

Да, это отвратительный взлом, но он работает

Ответ 4

Вот что я использовал в своей собственной библиотеке:

  • Объявить "внутренние" модули, которые объявляют классы без каких-либо круговых зависимостей.
  • Объявлять общедоступные модули, которые загружают внутренние модули в правильном порядке и добавляют любые методы, которые должны ссылаться на круговые зависимости.
  • Попросите пользователя импортировать любой из модулей с открытым доступом

внутренний /a.js

import C from './internal/c'

class A extends C {
    // ...
}

export {A as default}

внутренний /b.js

import C from './internal/c'

class B extends C {
    // ...
}

export {B as default}

внутренний /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}

Entrypoint

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

Ответ 5

Вы можете решить это с помощью динамически загружаемых модулей

У меня была такая же проблема, и я просто импортирую модули динамически.

Заменить по требованию импорт:

import module from 'module-path';

с динамическим импортом:

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

В вашем примере вы должны изменить c.js следующим образом:

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

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

Для получения дополнительной информации о динамическом импорте:

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

У leo есть другой способ объяснить это, только для ECMAScript 2019:

fooobar.com/info/1011971/...

Для анализа круговой зависимости Артур Хебда объясняет это здесь:

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