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

2024-02-18

编辑:有关更多背景,另请参阅ES 讨论 讨论 https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem.


我有三个模块A, B, and C. A and B从模块导入默认导出C,和模块C从两者导入默认值A and B。然而,模块C不依赖于导入的值A and B在模块评估期间,仅在所有三个模块评估完毕后的某个时刻运行时。模块A and B do取决于导入的值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 and B不会是undefined?

(我认为 ES6 Module 环境可能能够智能地发现它需要执行 module 的主体C在它可能执行模块主体之前A and B.)


答案是使用“init 函数”。作为参考,请查看从此处开始的两条消息: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

解决方案如下所示:

// --- 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 https://github.com/meteor/meteor/issues/7621#issuecomment-238992688

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

在此示例中,入口点从模块导入A,从模块导入C,从模块导入B。这意味着模块B将在模块之前评估C,但由于出口initC来自模块的函数C被吊装,模块B将给出对此提升的引用initC函数,因此模块B打电话 打电话initC模块之前C被评估。

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

需要注意的是,该模块C必须使用var C, not const or 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 calls initC,将会抛出错误,并且模块评估将失败。

var被吊装在模块范围内C,所以它可以用于当initC叫做。这是一个很好的例子,说明了您实际上想要使用的原因var代替let or const在 ES6+ 环境中。

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

最后要注意的重要一点是之间的区别export default C and export {C as default}。第一个版本does not导出C来自模块的变量C作为实时绑定,但是按值。所以,当export default C被使用时,其值var C is undefined并将被分配给一个新变量var default它隐藏在 ES6 模块范围内,并且由于以下事实:C被分配到default (as in var default = C按值,然后每当模块的默认导出C被另一个模块访问(例如模块B)另一个模块将进入模块C并访问的值default变量永远是undefined。所以如果模块C uses export default C,那么即使模块B calls initC (which does改变模块的值C的内部C变量),模块B实际上不会访问该内部C变量,它将访问default变量,仍然是undefined.

然而,当模块C使用表格export {C as default},ES6模块系统使用C变量作为默认导出变量而不是创建新的内部变量default多变的。这意味着C变量是实时绑定。任何时候模块依赖于模块C被评估,它将被给予模块C的内部C在给定时刻的变量,不是通过值,而是几乎就像将变量移交给另一个模块一样。所以,当模块B calls initC, 模块C的内部C变量被修改,模块B能够使用它是因为它具有对同一变量的引用(即使本地标识符不同)!基本上,在模块评估期间的任何时间,当一个模块将使用从另一个模块导入的标识符时,模块系统会进入另一个模块并及时获取该时刻的值。

我敢打赌大多数人不会知道它们之间的区别export default C and export {C as default},并且在许多情况下他们不需要这样做,但重要的是要了解在具有“init 函数”的模块之间使用“实时绑定”时的区别,以解决循环依赖关系,以及实时绑定可能有用的其他事情。不要离题太远,但如果您有一个单例,则可以使用活动绑定作为使模块范围成为单例对象的方法,并且活动绑定是访问单例中的事物的方式。

描述实时绑定发生的情况的一种方法是编写 javascript,其行为类似于上面的模块示例。这是模块B and C可能看起来像描述“实时绑定”的方式:

// --- 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 and function initC跨模块悬挂,因此模块B能够打电话initC然后使用C马上,之前var C and function initC在评估的代码中遇到。

当然,当模块使用不同的标识符时,情况会变得更加复杂,例如,如果模块B has import Blah from './c', then Blah仍将是一个实时绑定C模块变量C,但这并不容易像前面的例子那样使用正常的变量提升来描述,事实上Rollup 并不总是能正确处理它 https://github.com/rollup/rollup/issues/845.

假设我们有模块B如下和模块A and C是相同的:

// --- Module B

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

initC();

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

export {B as default}

那么如果我们使用纯 JavaScript 来仅描述模块发生的情况B and 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 and B取决于C 在模块评估时,不是在运行时。当模块A and B进行评估,然后要求C导出待定义。然而,当模块C被评估,它不依赖于A and B正在定义进口。模块C只需要使用A and B在未来的运行时,在评估所有模块之后,例如当入口点运行时new A()这将运行C构造函数。正是由于这个原因,模块C不需要initA or initB功能。

循环依赖中可能有多个模块需要相互依赖,在这种情况下,需要一种更复杂的“init 函数”解决方案。例如,假设模块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, the C模块将在之前进行评估A模块。这意味着console.log(A)模块顶部的语句C将记录undefined因为class A尚未定义。

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

现在,如果模块B想用A在评估期间,事情会变得更加复杂,但我将解决方案留给您想象......

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何修复这个 ES6 模块循环依赖? 的相关文章

随机推荐

  • 在 JavaScript 中。如何判断对象内部是否存在字段?

    当然 我想通过代码来完成此操作 并不是说我面临的这个问题没有其他选择 只是好奇 这将忽略通过原型链向下传递的属性 if obj hasOwnProperty field Do something
  • 即使我使用 MenuItemCompat.setOnActionExpandListener 我的应用程序也会崩溃,并建议使用 MenuItemCompat.setOnActionExpandListener [重复]

    这个问题在这里已经有答案了 今天 Android Studio 设置为稳定频道更新 让我下载 Android 支持存储库 46 0 0 的更新 所以我就这么做了 然后突然我们的应用程序每次都开始崩溃 因为MenuItemCompat set
  • 与 Backbone 交换视图?

    我环顾四周 但尚未找到解决以下问题的好方法 我有一个主干视图绑定到页面上的 el 它是传统意义上的 侧边栏 的容器元素 为了解释起见 这个侧边栏元素的inner html需要根据路线完全改变 然而 页面上的位置永远不会改变 并且将始终填充此
  • 缩略图和图像编号

    我正在使用 FancyBox 来显示组成一本书的大量页面 我使用 Fancybox 是因为它似乎是我尝试过的无数图库插件中唯一一个能够以合理的方式处理大于屏幕的图像的插件 我使用缩略图是因为我希望能够跳过图像 例如从图像 1 到图像 30
  • AWS Lambda - 在 Spring Boot 处理程序中注入 Spring Bean

    我已将一个简单的 Spring Boot 项目上传到 Amazon Lambda 目前正在尝试测试它 它可以 100 工作 但当我尝试注入 Spring Bean 时 我得到一个空指针 这是我的 LambdaHandler 代码 packa
  • 更改导航栏的字体

    这个问题很简单 但不幸的是答案并非如此 如何更改文本中的字体UINavigationBar 从 iOS 7 及更高版本开始 NSShadow shadow NSShadow new shadow shadowOffset CGSizeMak
  • SunTlsRsaPremasterSecret KeyGenerator 不可用

    当我的应用程序尝试从 JAVA 加载 RSA 算法提供程序类时 我遇到错误 异常堆栈如下 javax jms JMSException RSA premaster secret error at org apache activemq ut
  • Django1.9:'function'对象没有属性'_meta'

    Django 给出错误信息 形式 py from django import forms from django contrib auth import authenticate get user model login logout fr
  • 这个复杂的查询该怎么做呢?

    这是我的数据库架构 Post id title body date Tag id title Post Tag id id post id tag Comment id id post body date 帖子和标签之间存在多对多的关系 我
  • java.text.ParseException:无法解析的日期

    我在尝试以下代码时遇到解析异常 String date Sat Jun 01 12 53 10 IST 2013 SimpleDateFormat sdf new SimpleDateFormat MMM d yyyy HH mm ss D
  • 在 SQL 中选择连续数字

    这感觉很简单 但我在任何地方都找不到答案 我正在尝试按一天中的每个小时运行查询 所以我正在做一个Group By在小时部分 但并非所有小时都有数据 因此存在一些间隙 我想每小时显示一次 无论是否有数据 这是一个示例查询 SELECT DAT
  • 使用 unison 同步包含 git 存储库的目录

    我想在两台机器上同步包含我的点文件的 git 存储库的目录 两台机器都会对存储库中的文件进行更改 通常我使用一致来同步目录 但在这种情况下 文件在 git即使目录内容表面上相同 目录也会有所不同 Unison见状举手退出 unison do
  • 纤维与异步等待

    我正在加入一个 C 项目 其中开发人员大量使用Fibers https en wikipedia org wiki Fiber computer science 在这个项目之前我什至没有听说过它们并且以前使用过async await and
  • 重复消息之间的时间间隔

    根据 SQS 的文档 重点是我的 Amazon SQS 将消息副本存储在多个服务器上 以实现冗余和高可用性 在极少数情况下 当您接收或删除邮件时 存储邮件副本的服务器之一可能不可用 如果发生这种情况 该消息的副本将不会在该不可用的服务器上被
  • 如何在Spring Cloud Netflix eureka上注册Spring Boot微服务?

    我们计划使用 Spring Cloud Netflix oss 组件 所以我正在做一个小样本项目 我开发了 2 个 Spring 微服务 这些服务运行良好http localhost 9000 microsvc one http local
  • MySQL数据导出变化次数

    我有一些用于数据库的备份和恢复脚本 该表有一个时间戳字段 备份脚本如下所示 mysqldump u user ppass database tab fields terminated by skip comments table 它创建两个
  • Django SECURE_SSL_REDIRECT 和 301 HTTP 响应

    在与本地不同的环境中 我设置DJ SSL REDIRECT True对于我的 Django 项目 但现在 当我在另一个环境 例如 在 Travis CI 中 中运行时 与 REST API 端点相关的所有单元测试都失败了 发生的情况是所有
  • InkCanvas 的撤消/重做命令堆栈

    我正在使用 InkCanvas 创建类似绘画的应用程序 我愿意实现撤消 和我的应用程序中的重做功能 哪种是为 InkCanvas 实现撤消 重做的最佳方法 我已经为 WPF 应用程序实现了撤消 重做 并最终将我的撤消 重做代码发布到http
  • 完成 GeneratorDataset 迭代器时发生错误:已取消:操作已取消

    在运行具有使用tensorflow 2 0的代码的kubeflow管道时 每个纪元结束时显示以下错误 W tensorflow core kernels data generator dataset op cc 103 完成 Generat
  • 如何修复这个 ES6 模块循环依赖?

    编辑 有关更多背景 另请参阅ES 讨论 讨论 https esdiscuss org topic how to solve this basic es6 module circular dependency problem 我有三个模块A