编辑:有关更多背景信息,请参阅discussion on ES Discuss。
我有三个模块A
,B
和C
。 A
和B
从模块C
导入默认导出,模块C
从A
和B
导入默认导出。但是,模块C
不依赖于在模块评估期间从A
和B
导入的值,仅在运行时在所有三个模块评估之后的某个时刻。模块A
和B
执行取决于在模块评估期间从C
导入的值。
代码看起来像这样:
// --- Module A
import C from 'C'
class A extends C {
// ...
}
export {A as default}
// --- Module B
import C from 'C'
class B extends C {
// ...
}
export {B as default}
// --- Module C
import A from 'A'
import B from 'B'
class C {
constructor() {
// this may run later, after all three modules are evaluated, or
// possibly never.
console.log(A)
console.log(B)
}
}
export {C as default}
我有以下切入点:
// --- Entrypoint
import A from './app/A'
console.log('Entrypoint', A)
但是,实际发生的事情是首先评估模块B
,并且它在Chrome中失败并显示此错误(使用本机ES6类,而不是转换):
Uncaught TypeError: Class extends value undefined is not a function or null
这意味着,在评估模块C
时,模块B
中B
的值为undefined
,因为模块C
尚未评价。
您应该可以通过制作这四个文件并运行入口点文件来轻松复制。
我的问题是(我可以提出两个具体问题吗?):为什么负载顺序是这样的?如何编写循环相关的模块,以便它们可以工作,以便在评估C
和A
时B
的值不是undefined
?
(我认为ES6模块环境可能能够智能地发现它需要执行模块C
的主体才能执行模块的主体A
和{{ 1}}。)
答案 0 :(得分:12)
答案是使用" 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
重要的是要注意出口是悬挂的(可能很奇怪,你可以在es escucuss中询问以了解更多信息),就像var
一样,但是提升是在模块之间发生的。类不能被提升,但是函数可以是(就像它们在正常的ES6之前的范围中,但是在模块之间,因为导出是实时绑定,可能在它们被评估之前到达其他模块,几乎就像有一个范围包含所有模块,只能通过import
)访问标识符。
在此示例中,入口点从模块A
导入,模块C
从模块B
导入,从模块B
导入。这意味着将在模块C
之前评估模块initC
,但由于模块C
中导出的B
函数已挂起,因此模块initC
将引用此已提升的B
函数,因此在评估模块initC
之前调用C
模块var C
。
这会导致模块C
的{{1}}变量在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
时使用。{p> var
。这是一个很好的例子,说明您实际上想要在ES6 +环境中使用let
而不是const
或let C = C
。
但是,你可以注意汇总不能正确处理https://github.com/rollup/rollup/issues/845,并且可以在某些环境中使用看起来像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
的默认导出被另一个模块访问时(例如模块{{ 1}})另一个模块将进入模块C
并访问default
变量的值,该值始终为undefined
。因此,如果模块C
使用export default C
,即使模块B
调用initC
( 更改模块C
内部{{1}的值变量),模块C
实际上无法访问该内部B
变量,它将访问C
变量,该变量仍为default
。< / p>
但是,当模块undefined
使用C
形式时,ES6模块系统会使用export {C as default}
变量作为默认导出变量,而不是创建新的内部C
变量。这意味着default
变量是实时绑定。每当评估取决于模块C
的模块时,它将在给定时刻给出模块C
的内部C
变量,而不是值,但几乎像将变量移交给另一个模块。因此,当模块C
调用B
时,模块initC
的内部C
变量会被修改,模块C
可以使用它,因为它具有对同一变量的引用(即使本地标识符不同)!基本上,在模块评估期间的任何时候,当模块使用从另一个模块导入的标识符时,模块系统会到达另一个模块并在那个时刻获取值。
我敢打赌,大多数人都不知道B
和export default C
之间的区别,并且在许多情况下他们不需要,但重要的是要知道差异时使用&#34;实时绑定&#34;跨越模块的&#34; init函数&#34;为了解决循环依赖性,以及实时绑定可能有用的其他内容。不要过分偏离主题,但如果你有一个单例,可以使用活动绑定作为使模块作用域成为单例对象的方法,并使用实时绑定方式来访问单例中的内容。
描述实时绑定发生的事情的一种方法是编写与上述模块示例类似的javascript。这里有哪些模块export {C as default}
和B
可能以描述&#34;实时绑定&#34;的方式显示:
C
这有效地显示了ES6模块版本中发生的情况:首先评估B,但在模块中提升// --- 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()
和var C
,因此模块function initC
可以调用B
,然后在评估的代码中遇到initC
和C
之前立即使用var C
。
当然,当模块使用不同的标识符时会变得更复杂,例如,如果模块function initC
具有B
,那么import Blah from './c'
仍将是Blah
的实时绑定模块C
的变量,但使用常规变量提升并不是很容易描述,如上例所示,事实上Rollup isn't always handling it properly。
假设我们有模块C
如下,模块B
和A
是相同的:
C
然后,如果我们使用纯JavaScript来仅描述模块// --- Module B
import Blah, {initC} from './c';
initC();
console.log('Module B', Blah)
class B extends Blah {
// ...
}
export {B as default}
和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
函数。
循环依赖中的多个模块可能需要相互依赖,在这种情况下,更复杂的&#34; init函数&#34;需要解决方案。例如,假设模块initB
在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
,整个示例变得更加复杂(我已经省略了模块B和入口点,因为那些不会改变):
undefined
-
// --- 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
,事情就会变得更加复杂,但我会留下那个解决方案让你想象......
答案 1 :(得分:3)
我建议使用控制反转。通过添加A和B参数使您的C构造函数变为纯粹:
// --- Module A
import C from './C';
export default class A extends C {
// ...
}
// --- Module B
import C from './C'
export default class B extends C {
// ...
}
// --- Module C
export default class C {
constructor(A, B) {
// this may run later, after all three modules are evaluated, or
// possibly never.
console.log(A)
console.log(B)
}
}
// --- Entrypoint
import A from './A';
import B from './B';
import C from './C';
const c = new C(A, B);
console.log('Entrypoint', C, c);
document.getElementById('out').textContent = 'Entrypoint ' + C + ' ' + c;
https://www.webpackbin.com/bins/-KlDeP9Rb60MehsCMa8u
更新,以回复此评论:How to fix this ES6 module circular dependency?
或者,如果您不希望库使用者了解各种实现,您可以导出另一个隐藏这些细节的函数/类:
// Module ConcreteCImplementation
import A from './A';
import B from './B';
import C from './C';
export default function () { return new C(A, B); }
或使用此模式:
// --- Module A
import C, { registerA } from "./C";
export default class A extends C {
// ...
}
registerA(A);
// --- Module B
import C, { registerB } from "./C";
export default class B extends C {
// ...
}
registerB(B);
// --- Module C
let A, B;
const inheritors = [];
export const registerInheritor = inheritor => inheritors.push(inheritor);
export const registerA = inheritor => {
registerInheritor(inheritor);
A = inheritor;
};
export const registerB = inheritor => {
registerInheritor(inheritor);
B = inheritor;
};
export default class C {
constructor() {
// this may run later, after all three modules are evaluated, or
// possibly never.
console.log(A);
console.log(B);
console.log(inheritors);
}
}
// --- Entrypoint
import A from "./A";
import B from "./B";
import C from "./C";
const c = new C();
console.log("Entrypoint", C, c);
document.getElementById("out").textContent = "Entrypoint " + C + " " + c;
更新,以回复此评论:How to fix this ES6 module circular dependency?
要允许最终用户导入类的任何子集,只需创建一个lib.js文件,导出面向公众的api:
import A from "./A";
import B from "./B";
import C from "./C";
export { A, B, C };
或:
import A from "./A";
import B from "./B";
import C from "./ConcreteCImplementation";
export { A, B, C };
然后你可以:
// --- Entrypoint
import { C } from "./lib";
const c = new C();
const output = ["Entrypoint", C, c];
console.log.apply(console, output);
document.getElementById("out").textContent = output.join();
答案 2 :(得分:1)
还有另一种可能的解决方案..
// --- Entrypoint
import A from './app/A'
setTimeout(() => console.log('Entrypoint', A), 0)
是的,这是一个令人厌恶的黑客,但它确实有效
答案 3 :(得分:0)
这是一个对我有用的简单解决方案。我最初尝试trusktr's approach但是它触发了奇怪的eslint和IntelliJ IDEA警告(他们声称这个类没有被声明)。以下解决方案很好,因为它消除了依赖循环。没有魔力。
import
内部模块。import
触发依赖循环的模块。// Notice, we avoid importing any dependencies that could trigger loops.
// Importing external dependencies or internal dependencies that we know
// are safe is fine.
class C {
// OP's class didn't have any methods that didn't trigger
// a loop, but if it did, you'd declare them here.
}
export {C as default}
import C from './internal/c'
// NOTE: We must import './internal/c' first!
import A from 'A'
import B from 'B'
// See http://stackoverflow.com/a/9267343/14731 for why we can't replace
// "C.prototype.constructor" directly.
let temp = C.prototype;
C = function() {
// this may run later, after all three modules are evaluated, or
// possibly never.
console.log(A)
console.log(B)
}
C.prototype = temp;
// For normal methods, simply include:
// C.prototype.strippedMethod = function() {...}
export {C as default}
所有其他文件保持不变。
答案 4 :(得分:0)
您可以使用动态加载模块
来解决我遇到了同样的问题,我只是动态导入模块。
按需替换导入:
import module from 'module-path';
具有动态导入功能:
let module;
import('module-path').then((res)=>{
module = res;
});
在您的示例中,您应像这样更改 c.js :
import C from './internal/c'
let A;
let B;
import('./a').then((res)=>{
A = res;
});
import('./b').then((res)=>{
B = res;
});
// See http://stackoverflow.com/a/9267343/14731 for why we can't replace "C.prototype.constructor"
let temp = C.prototype;
C = function() {
// this may run later, after all three modules are evaluated, or
// possibly never.
console.log(A)
console.log(B)
}
C.prototype = temp;
export {C as default}
有关动态导入的更多信息:
http://2ality.com/2017/01/import-operator.html
狮子座还有另一种解释方法, 仅适用于ECMAScript 2019 :
https://stackoverflow.com/a/40418615/1972338
为了分析循环依赖关系, Artur Hebda 在这里进行解释:
https://railsware.com/blog/2018/06/27/how-to-analyze-circular-dependencies-in-es6/
答案 5 :(得分:0)
所有先前的答案都有些复杂。应该用“香草”进口产品解决吗?
您可以只使用一个主索引,从中导入所有符号。这很简单,JS可以解析它并解决循环导入。有一个really nice blog post描述了此解决方案,但这是根据OP的问题:
// --- Module A
import C from './index.js'
...
// --- Module B
import C from './index.js'
...
// --- Module C
import {A, B} from './index.js'
...
// --- index.js
import A from 'A'
import B from 'B'
import C from 'C'
export {A, B, C}
// --- Entrypoint
import A from './app/index.js'
console.log('Entrypoint', A)
评估顺序是index.js
(A-B-C)中的顺序。声明主体中的循环引用可以通过这种方式包括在内。因此,例如,如果B和C继承自A,但是A的方法包含对B或C的引用(如果正常导入,则将引发错误),这将起作用。