答案是使用“初始化函数”。作为参考,请查看从这里开始的两条消息: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
需要注意的是,exports 就像 一样被提升(可能很奇怪,你可以在 esdiscuss 中要求了解更多)var
,但提升是跨module发生的。类不能被提升,但函数可以(就像它们在正常的 ES6 之前的范围内,但跨module因为导出是实时绑定,可能在它们被评估之前到达其他module,几乎好像有一个范围包含所有标识符只能通过使用import
)访问的module。
在这个例子中,入口点从 moduleA
导入,从 moduleC
导入,从 module 导入B
。这意味着 moduleB
将在 module 之前被评估C
,但由于initC
从 module导出的函数C
被提升的事实, moduleB
将被赋予对这个被提升的initC
函数的引用,因此在评估module之前B
调用initC
moduleC
。
这会导致var C
module的变量在C
定义之前被class B extends C
定义。魔法!
需要注意的是,moduleC
必须使用var C
, not const
or let
,否则理论上应该在真正的 ES6 环境中抛出时间死区错误。例如,如果module 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;` !!
那么只要moduleB
调用initC
,就会抛出错误,module评估将失败。
var
被提升在 module 的范围内C
,所以它在initC
被调用时可用。这是一个原因,你真的想使用一个很好的例子var
,而不是let
或const
在ES6 +环境。
但是,您可以注意 rollup 无法正确处理此问题https://github.com/rollup/rollup/issues/845,并且看起来let C = C
可以在某些环境中使用的 hack 就像上面链接中指出的流星问题。
一个需要注意的最后一个重要的事情是之间的差异export default C
和export {C as default}
。第一个版本不会将C
module中的变量C
作为实时绑定导出,而是按值导出。因此,当export default C
使用时,var C
isundefined
和的值将被分配到一个var default
隐藏在 ES6 module范围内的新变量上,并且由于C
分配到default
(如var default = C
按值,那么每当module的默认导出C
是由另一个module(例如 module B
)访问,另一个module将进入 moduleC
并访问default
变量的值,该值始终是undefined
。因此,如果 moduleC
使用export default C
,那么即使moduleB
调用initC
(它确实改变了 moduleC
的内部C
变量的值), moduleB
实际上不会访问该内部C
变量,它将访问default
仍然是undefined
.
但是,当 moduleC
使用 form 时export {C as default}
,ES6 module系统使用该C
变量作为默认的导出变量,而不是创建一个新的内部default
变量。这意味着该C
变量是一个实时绑定。任何时候依赖于 module 的moduleC
被评估,它都会在给定时刻获得 moduleC
的内部C
变量,不是按值,而是几乎就像将变量交给另一个module一样。因此,当moduleB
调用时initC
,moduleC
的内部C
变量被修改,并且moduleB
能够使用它是因为它引用了同一个变量(即使本地标识符不同)!基本上,在module评估期间的任何时候,当一个module将使用它从另一个module导入的标识符时,module系统会进入另一个module并及时获取该值。
我敢打赌,大多数人不会知道export default C
and之间的区别export {C as default}
,并且在很多情况下他们不需要知道,但是在具有“init 函数”的module之间使用“实时绑定”以解决循环问题时,了解它们之间的区别很重要依赖关系,除此之外,实时绑定可能很有用。不要钻研太远的话题,但是如果您有一个单例,则可以使用活动绑定作为使module范围成为单例对象的一种方式,并且活动绑定是访问来自单例的事物的方式。
描述实时绑定发生的事情的一种方法是编写行为类似于上述module示例的 javascript。以下是描述“实时绑定”的moduleB
和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()
这说明有效地正在发生的事情在ES6module版本:B先进行评估,但var C
和function initC
整个module悬挂,所以moduleB
是能够调用initC
,然后使用C
向右走,之前var C
并function initC
在已评估的代码遇到。
当然,当module使用不同的标识符时它会变得更加复杂,例如如果moduleB
有import Blah from './c'
,那么Blah
仍然是C
对module变量的实时绑定C
,但这不像前面的例子那样使用普通的变量提升来描述,事实上,Rollup 并不总是正确处理它。
假设例如我们有B
如下moduleA
和module并且C
是相同的:
// --- Module B
import Blah, {initC} from './c';
initC();
console.log('Module B', Blah)
class B extends Blah {
// ...
}
export {B as default}
然后,如果我们使用纯 JavaScript 仅描述moduleB
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()
另外需要注意的是,moduleC
也有initC
函数调用。这是以防万一moduleC
首先被评估,然后初始化它不会有什么坏处。
最后要注意的是,在这些示例中,moduleA
和B
依赖C
于module评估时,而不是运行时。当moduleA
和B
被评估时,需要C
定义导出。然而,当moduleC
进行评估,它不依赖于A
与B
所定义的进口。moduleC
将只需要在未来运行时使用A
和B
,在评估所有module后,例如当入口点运行时new A()
将运行C
构造函数。正是因为这个原因,moduleC
不需要initA
或initB
起作用。
循环依赖中的多个module可能需要相互依赖,在这种情况下,需要一个更复杂的“初始化函数”解决方案。例如,假设moduleC
想要console.log(A)
在class C
定义之前的module评估时间:
// --- 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;` !!
由于顶部示例中的入口点 import A
,因此C
module将在A
module之前进行评估。这意味着console.log(A)
module顶部的语句C
将记录,undefined
因为class A
尚未定义。
最后,为了让新示例工作以便它记录class A
而不是undefined
,整个示例变得更加复杂(我已经省略了module 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;` !!
现在,如果moduleB
想A
在评估期间使用,事情会变得更加复杂,但我把这个解决方案留给你想象......