答案是使用“初始化函数”。作为参考,请查看从这里开始的两条消息: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调用initCmoduleC。
这会导致var Cmodule的变量在C定义之前被class B extends C定义。魔法!
需要注意的是,moduleC必须使用var C, not constor 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}。第一个版本不会将Cmodule中的变量C作为实时绑定导出,而是按值导出。因此,当export default C使用时,var Cisundefined和的值将被分配到一个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 Cand之间的区别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 仅描述moduleBand发生的事情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,因此Cmodule将在Amodule之前进行评估。这意味着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在评估期间使用,事情会变得更加复杂,但我把这个解决方案留给你想象......