如何解决此ES6模块循环依赖关系? [英] How to fix this ES6 module circular dependency?
问题描述
有关更多背景信息,另请参见 ES讨论.
for more background, also see the discussion on ES Discuss.
我有三个模块A
,B
和C
. A
和B
从模块C
导入默认导出,而模块C
从A
和B
导入默认导出.但是,模块C
不依赖于在模块评估期间从A
和B
导入的值,仅在运行时在评估了所有三个模块之后的某个时刻.模块A
和B
do 取决于模块评估期间从C
导入的值.
I have three modules A
, B
, and C
. A
and B
import the default export from module C
, and module C
imports the default from both A
and B
. However, module C
does not depend on the values imported from A
and B
during module evaluation, only at runtime at some point after all three modules have been evaluated. Modules A
and B
do depend on the value imported from C
during their module evaluation.
代码看起来像这样:
// --- 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类,而不是进行编译):
But, what actually happens is that module B
is evaluated first, and it fails with this error in Chrome (using native ES6 classes, not transpiling):
Uncaught TypeError: Class extends value undefined is not a function or null
这意味着在评估模块B
时,模块B
中C
的值为undefined
,因为尚未评估模块C
.
What that means is that the value of C
in module B
when module B
is being evaluated is undefined
because module C
has not yet been evaluated.
通过制作这四个文件并运行入口点文件,您应该能够轻松地进行复制.
You should be able to easily reproduce by making those four files, and running the entrypoint file.
我的问题是(我有两个具体的问题吗?):为什么装载顺序是这种方式?
My questions are (can I have two concrete questions?): Why is the load order that way? How can the circularly-dependent modules be written so that they will work so that the value of C
when evaluating A
and B
will not be undefined
?
(我认为ES6模块环境可能能够智能地发现它需要执行模块C
的主体,然后才可能执行模块A
和B
的主体.)
(I would think that the ES6 Module environment may be able to intelligently discover that it will need to execute the body of module C
before it can possibly execute the bodies of modules A
and B
.)
推荐答案
答案是使用初始函数".作为参考,请查看从此处开始的两条消息: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21
The answer is to use "init functions". For reference, look at the two messages starting here: 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/流星/流星/问题/7621#issuecomment-238992688
Also see this thread for related info: https://github.com/meteor/meteor/issues/7621#issuecomment-238992688
需要特别注意的是,就像var
一样,出口是悬挂的(可能很奇怪,您可以要求在esdiscuss中了解更多信息),但是悬挂是在各个模块之间进行的.无法提升类,但可以实现功能(就像它们在正常的ES6之前的作用域中一样,但是跨模块,因为导出是实时绑定,可能会在评估它们之前进入其他模块,就好像存在一个包含所有内容的作用域一样只能使用import
访问标识符的模块.
It is important to note that exports are hoisted (it may be strange, you can ask in esdiscuss to learn more) just like var
, but the hoisting happens across modules. Classes cannot be hoisted, but functions can be (just like they are in normal pre-ES6 scopes, but across modules because exports are live bindings that reach into other modules possibly before they are evaluated, almost as if there is a scope that encompasses all modules where identifiers can be accessed only through the use of import
).
在此示例中,入口点从模块A
导入,模块A
从模块C
导入,模块C
从模块B
导入.这意味着模块B
将在模块C
之前进行评估,但是由于从模块C
导出的initC
函数已被吊起,因此将为模块B
提供对此已举起的initC
的引用函数,因此模块B
调用在评估模块C
之前先调用initC
.
In this example, the entry point imports from module A
, which imports from module C
, which imports from module B
. This means module B
will be evaluated before module C
, but due to the fact that the exported initC
function from module C
is hoisted, module B
will be given a reference to this hoisted initC
function, and therefore module B
call call initC
before module C
is evaluated.
这将导致在class B extends C
定义之前定义模块C
的var C
变量.魔术!
This causes the var C
variable of module C
to become defined prior to the class B extends C
definition. Magic!
请务必注意,模块C
必须使用var C
,而不是const
或let
,否则,理论上应该在真正的ES6环境中引发时间死区错误.例如,如果模块C看起来像
It is important to note that module C
must use var C
, not const
or let
, otherwise a temporal deadzone error should theoretically be thrown in a true ES6 environment. For example, if module C looked like
// --- 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
,就会引发错误,并且模块评估将失败.
then as soon as module B
calls initC
, an error will be thrown, and the module evaluation will fail.
var
悬挂在模块C
的范围内,因此可在调用initC
时使用.这是一个很好的例子,说明了为什么您实际上希望在ES6 +环境中使用var
而不是let
或const
.
var
is hoisted within the scope of module C
, so it is available for when initC
is called. This is a great example of a reason why you'd actually want to use var
instead of let
or const
in an ES6+ environment.
但是,您可能会注意到汇总无法正确处理 https://github.com /rollup/rollup/issues/845 ,并且类似let C = C
的hack可以在某些环境中使用,如上面流星问题链接中指出的那样.
However, you can take note rollup doesn't handle this correctly https://github.com/rollup/rollup/issues/845, and a hack that looks like let C = C
can be used in some environments like pointed out in the above link to the Meteor issue.
要注意的最后一件事是export default C
和export {C as default}
之间的区别.第一个版本不会从模块C
导出C
变量作为实时绑定,而是按值导出.因此,当使用export default C
时,var C
的值为undefined
并将被分配给隐藏在ES6模块范围内的新变量var default
,并且由于分配了C
在default
上(按值在var default = C
中),然后每当另一个模块(例如,模块B
)访问模块C
的默认导出时,另一个模块将进入模块C
并访问该模块. default
变量的值始终为undefined
.因此,如果模块C
使用export default C
,则即使模块B
调用initC
(会更改 (模块C
的内部C
变量的值),模块B
实际上不会访问该内部C
变量,它将访问default
变量,该变量仍为undefined
.
One last important thing to note is the difference between export default C
and export {C as default}
. The first version does not export the C
variable from module C
as a live binding, but by value. So, when export default C
is used, the value of var C
is undefined
and will be assigned onto a new variable var default
that is hidden inside the ES6 module scope, and due to the fact that C
is assigned onto default
(as in var default = C
by value, then whenever the default export of module C
is accessed by another module (for example module B
) the other module will be reaching into module C
and accessing the value of the default
variable which is always going to be undefined
. So if module C
uses export default C
, then even if module B
calls initC
(which does change the values of module C
's internal C
variable), module B
won't actually be accessing that internal C
variable, it will be accessing the default
variable, which is still undefined
.
但是,当模块C
使用格式export {C as default}
时,ES6模块系统将C
变量用作默认的导出变量,而不是创建新的内部default
变量.这意味着C
变量是实时绑定.每当评估依赖于模块C
的模块时,都会在给定的时刻为模块C
的内部C
变量赋值,而不是通过值,而是就像将变量移交给另一个模块一样.因此,当模块B
调用initC
时,模块C
的内部C
变量将被修改,并且模块B
可以使用它,因为它具有对同一变量的引用(即使本地标识符不同)!基本上,在模块评估期间的任何时候,当一个模块将使用从另一个模块导入的标识符时,模块系统就会到达另一个模块并在该时刻获取值.
However, when module C
uses the form export {C as default}
, the ES6 module system uses the C
variable as the default exported variable rather than making a new internal default
variable. This means that the C
variable is a live binding. Any time a module depending on module C
is evaluated, it will be given the module C
's internal C
variable at that given moment, not by value, but almost like handing over the variable to the other module. So, when module B
calls initC
, module C
's internal C
variable gets modified, and module B
is able to use it because it has a reference to the same variable (even if the local identifier is different)! Basically, any time during module evaluation, when a module will use the identifier that it imported from another module, the module system reaches into the other module and gets the value at that moment in time.
我敢打赌大多数人不会知道export default C
和export {C as default}
之间的区别,在很多情况下他们并不需要,但是了解跨模块使用实时绑定"时的区别很重要为了解决循环依赖关系,其中包括实时函数"的初始化函数".不要太过离题,但是如果您有一个单例,则可以使用活动绑定作为使模块作用域成为单例对象的一种方式,而可以通过活动绑定来访问单例中的事物.
I bet most people won't know the difference between export default C
and export {C as default}
, and in many cases they won't need to, but it is important to know the difference when using "live bindings" across modules with "init functions" in order to solve circular dependencies, among other things where live bindings can be useful. Not to delve too far off topic, but if you have a singleton, alive bindings can be used as a way to make a module scope be the singleton object, and live bindings the way in which things from the singleton are accessed.
描述实时绑定所发生情况的一种方法是编写行为类似于上述模块示例的javascript.这是模块B
和C
的形式,它描述了实时绑定":
One way to describe what is happening with the live bindings is to write javascript that would behave similar to the above module example. Here's what modules B
and C
might look like in a way that describes the "live bindings":
// --- 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
,然后使用在评估的代码中遇到var C
和function initC
之前,立即C
.
This shows effectively what is happening in in the ES6 module version: B is evaluated first, but var C
and function initC
are hoisted across the modules, so module B
is able to call initC
and then use C
right away, before var C
and function initC
are encountered in the evaluated code.
当然,当模块使用不同的标识符时,情况会变得更加复杂,例如,如果模块B
具有import Blah from './c'
,则Blah
仍将是对模块C
的C
变量的实时绑定,但这很难像上一个示例中那样使用普通变量提升来描述,实际上汇总不是不一定总是正确处理.
Of course, it gets more complicated when modules use differing identifiers, for example if module B
has import Blah from './c'
, then Blah
will still be a live binding to the C
variable of module C
, but this is not very easy to describe using normal variable hoisting as in the previous example, and in fact Rollup isn't always handling it properly.
例如,假设我们有模块B
,而模块A
和C
相同:
Suppose for example we have module B
as the following and modules A
and C
are the same:
// --- Module B
import Blah, {initC} from './c';
initC();
console.log('Module B', Blah)
class B extends Blah {
// ...
}
export {B as default}
然后,如果我们使用普通的JavaScript仅描述模块B
和C
会发生什么,结果将是这样的:
Then if we use plain JavaScript to describe only what happens with modules B
and C
, the result would be like this:
// --- 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
进行了评估,然后对其进行初始化不会造成任何损害.
Another thing to note is that module C
also has the initC
function call. This is just in case module C
is ever evaluated first, it won't hurt to initialize it then.
最后要注意的是,在这些示例中,模块A
和B
在模块评估时依赖于C
,而不是在运行时依赖.评估模块A
和B
时,需要定义C
导出.但是,评估模块C
时,它不依赖于定义的A
和B
导入.在评估完所有模块之后,例如,当入口点运行new A()
时,模块C
将仅在将来的运行时使用A
和B
,而后者将运行C
构造函数.因此,模块C
不需要initA
或initB
功能.
And the last thing to note is that in these example, modules A
and B
depend on C
at module evaluation time, not at runtime. When modules A
and B
are evaluated, then require for the C
export to be defined. However, when module C
is evaluated, it does not depend on A
and B
imports being defined. Module C
will only need to use A
and B
at runtime in the future, after all modules are evaluated, for example when the entry point runs new A()
which will run the C
constructor. It is for this reason that module C
does not need initA
or initB
functions.
可能有多个模块需要相互依赖,并且在这种情况下,需要更复杂的初始化函数"解决方案.例如,假设模块C
希望在定义class C
之前的模块评估时间内访问console.log(A)
:
It is possible that more than one module in a circular dependency need to depend on each other, and in this case a more complex "init function" solution is needed. For example, suppose module C
wants to console.log(A)
during module evaluation time before class C
is defined:
// --- 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
,因此将在A
模块之前对C
模块进行评估.这意味着模块C
顶部的console.log(A)
语句将记录undefined
,因为尚未定义class A
.
Due to the fact that the entry point in the top example imports A
, the C
module will be evaluated before the A
module. This means that console.log(A)
statement at the top of module C
will log undefined
because class A
hasn't been defined yet.
最后,为了使新示例能够正常工作,使其记录为class A
而不是undefined
,整个示例变得更加复杂(我省略了模块B和入口点,因为它们没有改变):
Finally, to make the new example work so that it logs class A
instead of undefined
, the whole example becomes even more complicated (I've left out module B and the entry point, since those don't change):
// --- 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
,事情将会变得更加复杂,但是我让该解决方案让您想象...
Now, if module B
wanted to use A
during evaluation time, things would get even more complicated, but I leave that solution for you to imagine...
这篇关于如何解决此ES6模块循环依赖关系?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!