简体   繁体   English

如何修复这个 ES6 模块循环依赖?

[英]How to fix this ES6 module circular dependency?

EDIT: for more background, also see the discussion on ES Discuss .编辑:有关更多背景信息,另请参阅ES Discuss 上的讨论


I have three modules A , B , and C .我有三个模块ABC A and B import the default export from module C , and module C imports the default from both A and B . AB进口从模块的默认出口C和模块C进口都默认AB 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.但是,模块C不依赖于在模块评估期间从AB导入的值,仅在运行时在所有三个模块都已评估后的某个时间点。 Modules A and B do depend on the value imported from C during their module evaluation.模块AB确实依赖于在模块评估期间从C导入的值。

The code looks something like this:代码如下所示:

// --- 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}

I have the following entry point:我有以下入口点:

// --- Entrypoint

import A from './app/A'
console.log('Entrypoint', A)

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):但是,实际发生的是首先评估模块B ,它在 Chrome 中失败并出现此错误(使用原生 ES6 类,而不是转译):

Uncaught TypeError: Class extends value undefined is not a function or null

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.这意味着什么是,的值C在模块B当模块B正在评估是undefined ,因为模块C尚未评估。

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 ?如何编写循环依赖的模块,以便它们可以工作,以便在评估ABC的值不会是undefined

(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 .) (我认为 ES6 Module 环境可能能够智能地发现它需要先执行模块C的主体,然后才能执行模块AB的主体。)

The answer is to use "init functions".答案是使用“init 函数”。 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作为参考,请查看从这里开始的两条消息: https : //esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21

The solution looks like this:解决方案如下所示:

// --- 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.

Also see this thread for related info: https://github.com/meteor/meteor/issues/7621#issuecomment-238992688另请参阅此线程以获取相关信息: https : //github.com/meteor/meteor/issues/7621#issuecomment-238992688

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.需要注意的是,导出就像var一样被提升(可能很奇怪,你可以在 esdiscuss 中要求了解更多信息),但提升是跨模块发生的。 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 ).类不能被提升,但函数可以(就像它们在正常的 ES6 之前的范围内,但跨模块因为导出是实时绑定,可能在它们被评估之前到达其他模块,几乎好像有一个范围包含所有标识符只能通过使用import访问的模块)。

In this example, the entry point imports from module A , which imports from module C , which imports from module B .在此示例中,入口点从模块A导入,从模块C导入,从模块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.这意味着模块B将在模块C之前被评估,但由于从模块C导出的initC函数被提升的事实,模块B将被赋予对这个提升的initC函数的引用,因此模块B在模块C之前调用 call initC被评估。

This causes the var C variable of module C to become defined prior to the class B extends C definition.这会导致模块Cvar C变量在class B extends C定义之前被定义。 Magic!魔法!

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.需要注意的是,模块C必须使用var C ,而不是constlet ,否则理论上应该在真正的 ES6 环境中抛出时间死区错误。 For example, if module C looked like例如,如果模块 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;` !!

then as soon as module B calls initC , an error will be thrown, and the module evaluation will fail.那么只要模块B调用initC ,就会抛出错误,并且模块评估将失败。

var is hoisted within the scope of module C , so it is available for when initC is called. var在模块C的范围内提升,因此在initC时可用。 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.这是一个很好的例子,说明了在 ES6+ 环境中您实际上想要使用var而不是letconst的原因。

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.但是,您可以注意 rollup 没有正确处理这个https://github.com/rollup/rollup/issues/845 ,并且看起来像let C = C的黑客可以在某些环境中使用,如在上面链接到流星问题。

One last important thing to note is the difference between export default C and export {C as default} .最后一件需要注意的重要事情是export default Cexport {C as default}之间的区别。 The first version does not export the C variable from module C as a live binding, but by value.第一个版本不会将模块CC变量作为实时绑定导出,而是按值导出。 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 .因此,当使用export default C时, var C的值是undefined ,并将被分配到一个隐藏在 ES6 模块范围内的新变量var default上,并且由于C被分配到default (如var default = C by value,那么每当模块C的默认导出被另一个模块(例如模块B )访问时,另一个模块将进入模块C并访问default变量的值,该变量始终为undefined 。因此,如果模块C使用export default C ,那么即使模块B调用initC (它确实更改了模块C的内部C变量的值),模块B实际上不会访问该内部C变量,它将访问default变量,它仍然是undefined

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.但是,当模块C使用export {C as default} ,ES6 模块系统使用C变量作为默认导出变量,而不是创建一个新的内部default变量。 This means that the C variable is a live binding.这意味着C变量是实时绑定。 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.任何时候依赖模块C模块被评估时,它都会在给定时刻获得模块C的内部C变量,不是按值,而是几乎像将变量移交给另一个模块一样。 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)!因此,当模块B调用initC ,模块C的内部C变量被修改,并且模块B能够使用它,因为它引用了相同的变量(即使本地标识符不同)! 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.基本上,在模块评估期间的任何时候,当一个模块将使用它从另一个模块导入的标识符时,模块系统就会进入另一个模块并及时获取该值。

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.我敢打赌,大多数人不会知道export default Cexport {C as default}之间的区别,而且在很多情况下他们不需要知道,但是了解跨模块使用“实时绑定”时的区别很重要“init 函数”是为了解决循环依赖问题,以及实时绑定可能有用的其他问题。 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.不要钻研太远的话题,但是如果您有一个单例,则可以使用活动绑定作为使模块范围成为单例对象的一种方式,并且活动绑定是访问单例中事物的方式。

One way to describe what is happening with the live bindings is to write javascript that would behave similar to the above module example.描述实时绑定发生的事情的一种方法是编写 javascript,其行为类似于上述模块示例。 Here's what modules B and C might look like in a way that describes the "live bindings":以下是模块BC以描述“实时绑定”的方式可能的样子:

// --- 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()

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.这有效地显示了 ES6 模块版本中发生的事情:首先评估 B,但是var Cfunction initC被提升到模块之间,因此模块B能够调用initC然后立即使用C ,在var Cfunction initC之前function initC在评估代码中遇到。

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 import Blah from './c' ,那么Blah仍然是到模块CC变量的实时绑定,但这不是很容易像前面的例子一样描述使用普通变量提升,实际上Rollup 并不总是正确处理它

Suppose for example we have module B as the following and modules A and C are the same:例如,假设我们有如下的模块B ,模块AC是相同的:

// --- Module B

import Blah, {initC} from './c';

initC();

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

export {B as default}

Then if we use plain JavaScript to describe only what happens with modules B and C , the result would be like this:然后,如果我们使用纯 JavaScript 仅描述模块BC发生的事情,结果将是这样的:

// --- 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()

Another thing to note is that module C also has the initC function call.另一个需要注意的是,模块C也有initC函数调用。 This is just in case module C is ever evaluated first, it won't hurt to initialize it then.以防万一模块C被首先评估,然后初始化它不会有什么坏处。

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.最后要注意的是,在这些示例中,模块AB在模块评估时依赖于C ,而不是在运行时。 When modules A and B are evaluated, then require for the C export to be defined.评估模块AB ,需要定义C导出。 However, when module C is evaluated, it does not depend on A and B imports being defined.但是,在评估模块C时,它不依赖于定义的AB导入。 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.模块C将来只需要在运行时使用AB ,在评估所有模块之后,例如当入口点运行将运行C构造函数的new A() It is for this reason that module C does not need initA or initB functions.正是因为这个原因,模块C不需要initAinitB函数。

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.循环依赖中的多个模块可能需要相互依赖,在这种情况下,需要一个更复杂的“init 函数”解决方案。 For example, suppose module C wants to console.log(A) during module evaluation time before class C is defined:例如,假设模块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;` !!

Due to the fact that the entry point in the top example imports A , the C module will be evaluated before the A module.由于顶部示例中的入口点导入A ,因此将在A模块之前评估C模块。 This means that console.log(A) statement at the top of module C will log undefined because class A hasn't been defined yet.这意味着模块C顶部的console.log(A)语句将记录undefined因为class A尚未定义。

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):最后,为了使新示例工作,以便记录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;` !!

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...现在,如果模块B想在评估期间使用A ,事情会变得更加复杂,但我把这个解决方案留给你想象......

I would recommend to use inversion of control.我建议使用控制反转。 Make your C constructor pure by adding an A and a B parameter like this:通过添加 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 https://www.webpackbin.com/bins/-KlDeP9Rb60MehsCMa8u

Update, in response to this comment: How to fix this ES6 module circular dependency?更新,响应此评论: 如何修复此 ES6 模块循环依赖?

Alternatively, if you do not want the library consumer to know about various implementations, you can either export another function/class that hides those details:或者,如果您不希望库使用者了解各种实现,您可以导出另一个隐藏这些细节的函数/类:

// Module ConcreteCImplementation
import A from './A';
import B from './B';
import C from './C';
export default function () { return new C(A, B); }

or use this pattern:或使用此模式:

// --- 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;

Update, in response to this comment: How to fix this ES6 module circular dependency?更新,响应此评论: 如何修复此 ES6 模块循环依赖?

To allow the end-user to import any subset of the classes, just make a lib.js file exporting the public facing api:要允许最终用户导入类的任何子集,只需创建一个导出面向公众的 api 的 lib.js 文件:

import A from "./A";
import B from "./B";
import C from "./C";
export { A, B, C };

or:或:

import A from "./A";
import B from "./B";
import C from "./ConcreteCImplementation";
export { A, B, C };

Then you can:然后你可以:

// --- 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();

There is another possible solution..还有另一种可能的解决方案..

// --- Entrypoint

import A from './app/A'
setTimeout(() => console.log('Entrypoint', A), 0)

Yes it's a disgusting hack but it works是的,这是一个令人作呕的黑客,但它有效

All the previous answers are a bit complex.之前的所有答案都有点复杂。 Shouldn't this be solved with "vanilla" imports?这不应该用“香草”进口来解决吗?

You can just use a single master index, from which all symbols are imported.您可以只使用一个主索引,从中导入所有符号。 This is simple enough that JS can parse it and solve the circular import.这很简单,JS可以解析它并解决循环导入。 There's a really nice blog post that describes this solution, but here it is according to the OP's question:有一篇非常好的博客文章描述了这个解决方案,但这里是根据 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 C from 'C'
import A from 'A'
import B from 'B'
export {A, B, C}

// --- Entrypoint

import A from './app/index.js'
console.log('Entrypoint', A)

The order of evaluation is the order in index.js (CAB).求值顺序是index.js (CAB) 中的顺序。 Circular references in the body of declarations can be included this way.声明主体中的循环引用可以通过这种方式包含在内。 So, for example, if B and C inherit from A, but A's methods contain references to B or C (which would throw an error if importing normally), this will work.因此,例如,如果 B 和 C 从 A 继承,但 A 的方法包含对 B 或 C 的引用(如果正常导入会引发错误),这将起作用。

Here is a simple solution that worked for me.这是一个对我有用的简单解决方案。 I initially tried trusktr's approach but it triggered weird eslint and IntelliJ IDEA warnings (they claimed the class was not declared when it was).我最初尝试了trusktr 的方法,但它触发了奇怪的 eslint 和 IntelliJ IDEA 警告(他们声称该类没有被声明)。 The following solution is nice because it eliminates the dependency loops.以下解决方案很好,因为它消除了依赖循环。 No magic.没有魔法。

  1. Split the class with circular dependencies into two pieces: the code that triggers the loop and the code that does not.将具有循环依赖关系的类分成两部分:触发循环的代码和不触发循环的代码。
  2. Place the code that does not trigger a loop into an "internal" module.将不会触发循环的代码放入“内部”模块中。 In my case, I declared the superclass and stripped out any methods that referenced subclasses.就我而言,我声明了超类并删除了所有引用子类的方法。
  3. Create a public-facing module.创建一个面向公众的模块。
    • import the internal module first. import内部模块。
    • import the modules that triggered the dependency loop. import触发依赖循环的模块。
    • Add back the methods that we stripped out in step 2.重新添加我们在第 2 步中删除的方法。
  4. Have the user import the public-facing module.让用户导入面向公众的模块。

OP's example is a little contrived because adding a constructor in step 3 is a lot harder than adding normal methods but the general concept remains the same. OP 的示例有点做作,因为在步骤 3 中添加构造函数比添加普通方法要困难得多,但一般概念保持不变。

internal/c.js内部/c.js

// 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}

c.js js

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}

All other files remain unchanged.所有其他文件保持不变。

You can Solve it with dynamically loading modules您可以通过动态加载模块来解决它

I had same problem and i just import modules dynamically.我有同样的问题,我只是动态导入模块。

Replace on demand import:替换按需导入:

import module from 'module-path';

with dynamically import:动态导入:

let module;
import('module-path').then((res)=>{
    module = res;
});

In your example you should change c.js like this:在您的示例中,您应该像这样更改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}

For more information about dynamically import:有关动态导入的更多信息:

http://2ality.com/2017/01/import-operator.html http://2ality.com/2017/01/import-operator.html

There is another way explain by leo, it just for ECMAScript 2019 : leo解释了另一种方式,它仅适用于 ECMAScript 2019

https://stackoverflow.com/a/40418615/1972338 https://stackoverflow.com/a/40418615/1972338

For analyzing circular dependency, Artur Hebda explain it here:为了分析循环依赖, Artur Hebda在这里解释:

https://railsware.com/blog/2018/06/27/how-to-analyze-circular-dependencies-in-es6/ https://railsware.com/blog/2018/06/27/how-to-analyze-circular-dependencies-in-es6/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM