gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-02 19:07:27 25 4
gpt4 key购买 nike

编辑:有关更多背景信息,另请参阅 discussion on ES Discuss

我有三个模块 ABCAB 从模块 C 导入默认导出,而模块 C 从 0x35134313131231312313123123123123123123123131231231312313123123都分别导入默认值。但是,模块 A 不依赖于在模块评估期间从 BC 导入的值,仅在评估所有三个模块后的某个时间点运行。模块 AB 确实取决于在模块评估期间从 A 导入的值。

代码如下所示:

// --- 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 中的 0x2518122231343141 的值为 C,因为模块 0x23113 尚未被评估13。

通过制作这四个文件并运行入口点文件,您应该能够轻松重现。

我的问题是(我可以有两个具体的问题吗?):为什么加载顺序是这样的?如何编写循环依赖模块,以便它们可以工作,以便在评估 BBundefined 的值不会是 0x251812231343144?

(我认为 ES6 Module 环境可能会智能地发现它需要执行模块 C 的主体,然后才能执行模块 CA 的主体。)

最佳答案

答案是使用“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

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

在这个例子中,入口点从模块 A 导入,模块从模块 C 导入,模块从模块 B 导入。这意味着模块 B将模块 C之前评估,但由于这样的事实,所述从模块 initC导出 C功能悬挂,模块 B将给予这个引用悬挂 initC功能,和模块 B之前因此模块 initC调用调用 C被评估。

这会导致模块 var CC 变量在 class B extends C 定义之前被定义。魔法!

需要注意的是,模块 C 必须使用 var C ,而不是 constlet ,否则理论上应该会在真正的 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 而不是 letconst 的原因。

但是,您可以注意 rollup 没有正确处理这个 https://github.com/rollup/rollup/issues/845 ,并且看起来像 let C = C 的黑客可以在某些环境中使用,如上面指向 Meteor 问题的链接中指出的那样。

最后一件需要注意的重要事情是 export default Cexport {C as default} 之间的区别。第一个版本不会将 C 变量从模块 C 导出为实时绑定(bind),而是按值导出。所以,当 export default C使用,的 var C值为 undefined和将被分配到一个新的变量 var default被隐藏在ES6模块范围内,并且由于其 C被分配到 default(如在 var default = C由值,则实际上每当模块 C的默认导出被另一个模块进行访问(例如模块 B)的其他模块将被到达入模块 C和访问它总是要 default所述的 undefined变量的值。因此,如果模块 C用途 export default C,那么,即使模块 B调用 initC(这会改变模块 C的内部 C变量的值),模块 B实际上不会访问该内部变量 C,它将被访问 default VA riable,它仍然是 undefined

但是,当模块 C 使用形式 export {C as default} 时,ES6 模块系统使用 C 变量作为默认导出变量,而不是创建一个新的内部 0x2518112431343 变量。这意味着 default 变量是一个实时绑定(bind)。任何时候评估依赖于模块 C 的模块时,都会在给定时刻为模块 C 的内部变量 C 赋值,不是按值,而是几乎像将变量移交给另一个模块一样。因此,当模块 C 调用 B 时,模块 initC 的内部 C 变量被修改,并且模块 02314 使用不同的标识符(因为它具有相同的局部标识符),因为它具有相同的局部标识符基本上,在模块评估期间的任何时候,当一个模块将使用它从另一个模块导入的标识符时,模块系统就会进入另一个模块并及时获取该值。

我敢打赌,大多数人不会知道 CB 之间的区别,并且在许多情况下他们不需要,但重要的是要知道在具有“init 函数”的模块之间使用“实时绑定(bind)”时的区别,以便解决循环依赖,以及实时绑定(bind)可能有用的其他问题。不要钻研太远的话题,但是如果您有一个单例,则可以使用事件绑定(bind)作为使模块范围成为单例对象的一种方式,并且事件绑定(bind)是访问单例中事物的方式。

描述实时绑定(bind)发生的事情的一种方法是编写 javascript,其行为类似于上述模块示例。以下是模块 export default Cexport {C as default} 可能看起来像描述“实时绑定(bind)”的方式:
// --- 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先进行评估,但 BC被跨模块悬挂,所以模块 var C能够调用 function initC然后用 B向右走, initC之前和 C在遇到评估的代码。

当然,当模块使用不同的标识符时会变得更加复杂,例如,如果模块 var Cfunction initC ,那么 B 仍然是一个容易绑定(bind)到 0x2518122231343141 的模块,但是使用这个变量很容易,但是使用 0x2518122223131313133131313131313131313 变量在前面的例子中,实际上是 Rollup isn't always handling it properly

例如,假设我们有模块 import Blah from './c' 如下,模块 BlahC 是相同的:
// --- Module B

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

initC();

console.log('Module B', Blah)

class B extends Blah {
// ...
}

export {B as default}

然后,如果我们使用纯 JavaScript 仅描述模块 CB 发生的情况,结果将如下所示:
// --- 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()

另外需要注意的是,模块 A 也有 C 函数调用。这是为了防止模块 B 首先被评估,然后初始化它不会有什么坏处。

最后要注意的是,在这些示例中,模块 CC 在模块评估时依赖于 initC,而不是在运行时。当模块 CA 被评估时,需要定义 B 导出。但是,当评估模块 C 时,它不依赖于定义的 AB 导入。模块 C 将只需要在 future 运行时使用 CA,在所有模块都被评估后,例如当入口点运行时 0x25181223134513431343131313431313134313131313131313131314313131431313143131313131313143131431313131431313131313131313143131313143143143143143143143143143123131313131313131313131313131313131313131313都被评估后,所有模块都需要在运行时使用。正是因为这个原因,模块 B 不需要 CA 函数。

循环依赖中的多个模块可能需要相互依赖,在这种情况下,需要一个更复杂的“init 函数”解决方案。例如,假设模块 B 在定义 new A() 之前的模块评估时间内想要 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;` !!

由于顶部示例中的入口点导入 C ,因此 initA 模块将在 initB 模块之前进行评估。这意味着模块 C 顶部的 console.log(A) 语句将记录 class C,因为 A 尚未定义。

最后,为了使新示例工作,以便它记录 C 而不是 A ,整个示例变得更加复杂(我省略了模块 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;` !!

现在,如果模块 console.log(A) 想在评估期间使用 C,事情会变得更加复杂,但我把这个解决方案留给你想象......

关于javascript - 如何修复这个 ES6 模块循环依赖?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38841469/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com