如何解决此ES6模块循环依赖关系? [英] How to fix this ES6 module circular dependency?

查看:198
本文介绍了如何解决此ES6模块循环依赖关系?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有关更多背景信息,另请参见 ES讨论.

for more background, also see the discussion on ES Discuss.

我有三个模块ABC. AB从模块C导入默认导出,而模块CAB导入默认导出.但是,模块C不依赖于在模块评估期间从AB导入的值,仅在运行时在评估了所有三个模块之后的某个时刻.模块AB 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时,模块BC的值为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的主体,然后才可能执行模块AB的主体.)

(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定义之前定义模块Cvar 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,而不是constlet,否则,理论上应该在真正的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而不是letconst.

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 Cexport {C as default}之间的区别.第一个版本不会从模块C导出C变量作为实时绑定,而是按值导出.因此,当使用export default C时,var C的值为undefined并将被分配给隐藏在ES6模块范围内的新变量var default,并且由于分配了Cdefault上(按值在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 Cexport {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.这是模块BC的形式,它描述了实时绑定":

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 Cfunction initC,因此模块B可以调用initC,然后使用在评估的代码中遇到var Cfunction 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仍将是对模块CC变量的实时绑定,但这很难像上一个示例中那样使用普通变量提升来描述,实际上汇总不是不一定总是正确处理.

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,而模块AC相同:

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仅描述模块BC会发生什么,结果将是这样的:

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.

最后要注意的是,在这些示例中,模块AB在模块评估时依赖于C ,而不是在运行时依赖.评估模块AB时,需要定义C导出.但是,评估模块C时,它不依赖于定义的AB导入.在评估完所有模块之后,例如,当入口点运行new A()时,模块C将仅在将来的运行时使用AB,而后者将运行C构造函数.因此,模块C不需要initAinitB功能.

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆