答案是使用“init 函数”。作为参考,请查看从此处开始的两条消息:https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21 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 https://github.com/meteor/meteor/issues/7621#issuecomment-238992688
需要注意的是,exports 是被吊起的(可能有点奇怪,可以在 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
, not const
or 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
calls initC
,将会抛出错误,并且模块评估将失败。
var
被吊装在模块范围内C
,所以它可以用于当initC
叫做。这是一个很好的例子,说明了您实际上想要使用的原因var
代替let
or const
在 ES6+ 环境中。
但是,您可以注意 rollup 不能正确处理这个问题https://github.com/rollup/rollup/issues/845 https://github.com/rollup/rollup/issues/845,以及一个看起来像这样的黑客let C = C
可以在某些环境中使用,如上面的 Meteor 问题链接中指出的那样。
最后要注意的重要一点是之间的区别export default C
and export {C as default}
。第一个版本does not导出C
来自模块的变量C
作为实时绑定,但是按值。所以,当export default C
被使用时,其值var C
is undefined
并将被分配给一个新变量var default
它隐藏在 ES6 模块范围内,并且由于以下事实:C
被分配到default
(as in var default = C
按值,然后每当模块的默认导出C
被另一个模块访问(例如模块B
)另一个模块将进入模块C
并访问的值default
变量永远是undefined
。所以如果模块C
uses export default C
,那么即使模块B
calls initC
(which does改变模块的值C
的内部C
变量),模块B
实际上不会访问该内部C
变量,它将访问default
变量,仍然是undefined
.
然而,当模块C
使用表格export {C as default}
,ES6模块系统使用C
变量作为默认导出变量而不是创建新的内部变量default
多变的。这意味着C
变量是实时绑定。任何时候模块依赖于模块C
被评估,它将被给予模块C
的内部C
在给定时刻的变量,不是通过值,而是几乎就像将变量移交给另一个模块一样。所以,当模块B
calls initC
, 模块C
的内部C
变量被修改,模块B
能够使用它是因为它具有对同一变量的引用(即使本地标识符不同)!基本上,在模块评估期间的任何时间,当一个模块将使用从另一个模块导入的标识符时,模块系统会进入另一个模块并及时获取该时刻的值。
我敢打赌大多数人不会知道它们之间的区别export default C
and export {C as default}
,并且在许多情况下他们不需要这样做,但重要的是要了解在具有“init 函数”的模块之间使用“实时绑定”时的区别,以解决循环依赖关系,以及实时绑定可能有用的其他事情。不要离题太远,但如果您有一个单例,则可以使用活动绑定作为使模块范围成为单例对象的方法,并且活动绑定是访问单例中的事物的方式。
描述实时绑定发生的情况的一种方法是编写 javascript,其行为类似于上面的模块示例。这是模块B
and 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
and function initC
跨模块悬挂,因此模块B
能够打电话initC
然后使用C
马上,之前var C
and function initC
在评估的代码中遇到。
当然,当模块使用不同的标识符时,情况会变得更加复杂,例如,如果模块B
has import Blah from './c'
, then Blah
仍将是一个实时绑定C
模块变量C
,但这并不容易像前面的例子那样使用正常的变量提升来描述,事实上Rollup 并不总是能正确处理它 https://github.com/rollup/rollup/issues/845.
假设我们有模块B
如下和模块A
and C
是相同的:
// --- Module B
import Blah, {initC} from './c';
initC();
console.log('Module B', Blah)
class B extends Blah {
// ...
}
export {B as default}
那么如果我们使用纯 JavaScript 来仅描述模块发生的情况B
and 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
and B
取决于C
在模块评估时,不是在运行时。当模块A
and B
进行评估,然后要求C
导出待定义。然而,当模块C
被评估,它不依赖于A
and B
正在定义进口。模块C
只需要使用A
and B
在未来的运行时,在评估所有模块之后,例如当入口点运行时new A()
这将运行C
构造函数。正是由于这个原因,模块C
不需要initA
or 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
, the 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
在评估期间,事情会变得更加复杂,但我将解决方案留给您想象......