答案是使用“初始化函数”。作为参考,请查看从此处开始的两条消息: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 之前被评估,但是由于从模块C 导出的initC 函数被提升,模块B 将被引用这个提升的initC函数,因此在评估模块 C 之前,模块 B 调用 call initC。
这会导致模块C 的var 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 时可用。这是一个很好的例子,说明了为什么您实际上想要在 ES6+ 环境中使用 var 而不是 let 或 const。
但是,您可以注意,rollup 无法正确处理此问题https://github.com/rollup/rollup/issues/845,并且看起来像 let C = C 的 hack 可以在某些环境中使用,如上面指向 Meteor 问题的链接中指出的那样。
最后要注意的重要一点是export default C 和export {C as default} 之间的区别。第一个版本不从模块C 导出C 变量作为实时绑定,而是按值导出。因此,当使用export default C 时,var C 的值是undefined 并将分配给隐藏在 ES6 模块范围内的新变量 var default,并且由于分配了 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 的内部C 变量,而不是按值,但几乎就像将变量移交给另一个模块一样。因此,当模块B 调用initC 时,模块C 的内部C 变量被修改,并且模块B 能够使用它,因为它引用了相同的变量(即使本地标识符不同)!基本上,在模块评估期间的任何时候,当一个模块将使用它从另一个模块导入的标识符时,模块系统会到达另一个模块并及时获取该时刻的值。
我敢打赌,大多数人不会知道 export default C 和 export {C as default} 之间的区别,而且在许多情况下他们也不需要知道,但是在跨模块使用“实时绑定”时了解区别很重要“init 函数”以解决循环依赖关系,其中实时绑定可能很有用。不要深入研究主题,但如果你有一个单例,可以使用活动绑定作为使模块范围成为单例对象的一种方式,并且可以使用活动绑定来访问单例中的内容。
描述实时绑定发生的事情的一种方法是编写行为类似于上述模块示例的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 变量的实时绑定C,但这并不像前面的例子那样使用普通的变量提升来描述,实际上是Rollup isn't always handling it properly。
假设我们有模块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 函数。
循环依赖中的多个模块可能需要相互依赖,在这种情况下,需要更复杂的“初始化函数”解决方案。例如,假设模块C 想要在定义class C 之前的模块评估期间console.log(A):
// --- 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 模块之前进行评估。这意味着模块C 顶部的console.log(A) 语句将记录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,事情会变得更加复杂,但我把这个解决方案留给你想象......