Ответ 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
во время оценки, все будет еще сложнее, но я оставляю это решение для вас...