簡體   English   中英

如何修復這個 ES6 模塊循環依賴?

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

編輯:有關更多背景信息,另請參閱ES Discuss 上的討論


我有三個模塊ABC AB進口從模塊的默認出口C和模塊C進口都默認AB 但是,模塊C不依賴於在模塊評估期間從AB導入的值,僅在運行時在所有三個模塊都已評估后的某個時間點。 模塊AB確實依賴於在模塊評估期間從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尚未評估。

通過制作這四個文件並運行入口點文件,您應該能夠輕松重現。

我的問題是(我可以有兩個具體的問題嗎?):為什么加載順序是這樣的? 如何編寫循環依賴的模塊,以便它們可以工作,以便在評估ABC的值不會是undefined

(我認為 ES6 Module 環境可能能夠智能地發現它需要先執行模塊C的主體,然后才能執行模塊AB的主體。)

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

需要注意的是,導出就像var一樣被提升(可能很奇怪,你可以在 esdiscuss 中要求了解更多信息),但提升是跨模塊發生的。 類不能被提升,但函數可以(就像它們在正常的 ES6 之前的范圍內,但跨模塊因為導出是實時綁定,可能在它們被評估之前到達其他模塊,幾乎好像有一個范圍包含所有標識符只能通過使用import訪問的模塊)。

在此示例中,入口點從模塊A導入,從模塊C導入,從模塊B導入。 這意味着模塊B將在模塊C之前被評估,但由於從模塊C導出的initC函數被提升的事實,模塊B將被賦予對這個提升的initC函數的引用,因此模塊B在模塊C之前調用 call initC被評估。

這會導致模塊Cvar C變量在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的黑客可以在某些環境中使用,如在上面鏈接到流星問題。

最后一件需要注意的重要事情是export default Cexport {C as default}之間的區別。 第一個版本不會將模塊CC變量作為實時綁定導出,而是按值導出。 因此,當使用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

但是,當模塊C使用export {C as default} ,ES6 模塊系統使用C變量作為默認導出變量,而不是創建一個新的內部default變量。 這意味着C變量是實時綁定。 任何時候依賴模塊C模塊被評估時,它都會在給定時刻獲得模塊C的內部C變量,不是按值,而是幾乎像將變量移交給另一個模塊一樣。 因此,當模塊B調用initC ,模塊C的內部C變量被修改,並且模塊B能夠使用它,因為它引用了相同的變量(即使本地標識符不同)! 基本上,在模塊評估期間的任何時候,當一個模塊將使用它從另一個模塊導入的標識符時,模塊系統就會進入另一個模塊並及時獲取該值。

我敢打賭,大多數人不會知道export default Cexport {C as default}之間的區別,而且在很多情況下他們不需要知道,但是了解跨模塊使用“實時綁定”時的區別很重要“init 函數”是為了解決循環依賴問題,以及實時綁定可能有用的其他問題。 不要鑽研太遠的話題,但是如果您有一個單例,則可以使用活動綁定作為使模塊范圍成為單例對象的一種方式,並且活動綁定是訪問單例中事物的方式。

描述實時綁定發生的事情的一種方法是編寫 javascript,其行為類似於上述模塊示例。 以下是模塊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()

這有效地顯示了 ES6 模塊版本中發生的事情:首先評估 B,但是var Cfunction initC被提升到模塊之間,因此模塊B能夠調用initC然后立即使用C ,在var Cfunction initC之前function initC在評估代碼中遇到。

當然,當模塊使用不同的標識符時會變得更復雜,例如,如果模塊B import Blah from './c' ,那么Blah仍然是到模塊CC變量的實時綁定,但這不是很容易像前面的例子一樣描述使用普通變量提升,實際上Rollup 並不總是正確處理它

例如,假設我們有如下的模塊B ,模塊AC是相同的:

// --- Module B

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

initC();

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

export {B as default}

然后,如果我們使用純 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()

另一個需要注意的是,模塊C也有initC函數調用。 以防萬一模塊C被首先評估,然后初始化它不會有什么壞處。

最后要注意的是,在這些示例中,模塊AB在模塊評估時依賴於C ,而不是在運行時。 評估模塊AB ,需要定義C導出。 但是,在評估模塊C時,它不依賴於定義的AB導入。 模塊C將來只需要在運行時使用AB ,在評估所有模塊之后,例如當入口點運行將運行C構造函數的new A() 正是因為這個原因,模塊C不需要initAinitB函數。

循環依賴中的多個模塊可能需要相互依賴,在這種情況下,需要一個更復雜的“init 函數”解決方案。 例如,假設模塊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;` !!

由於頂部示例中的入口點導入A ,因此將在A模塊之前評估C模塊。 這意味着模塊C頂部的console.log(A)語句將記錄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 ,事情會變得更加復雜,但我把這個解決方案留給你想象......

我建議使用控制反轉。 通過添加 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

更新,響應此評論: 如何修復此 ES6 模塊循環依賴?

或者,如果您不希望庫使用者了解各種實現,您可以導出另一個隱藏這些細節的函數/類:

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

或使用此模式:

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

更新,響應此評論: 如何修復此 ES6 模塊循環依賴?

要允許最終用戶導入類的任何子集,只需創建一個導出面向公眾的 api 的 lib.js 文件:

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

或:

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

然后你可以:

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

還有另一種可能的解決方案..

// --- Entrypoint

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

是的,這是一個令人作嘔的黑客,但它有效

之前的所有答案都有點復雜。 這不應該用“香草”進口來解決嗎?

您可以只使用一個主索引,從中導入所有符號。 這很簡單,JS可以解析它並解決循環導入。 有一篇非常好的博客文章描述了這個解決方案,但這里是根據 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)

求值順序是index.js (CAB) 中的順序。 聲明主體中的循環引用可以通過這種方式包含在內。 因此,例如,如果 B 和 C 從 A 繼承,但 A 的方法包含對 B 或 C 的引用(如果正常導入會引發錯誤),這將起作用。

這是一個對我有用的簡單解決方案。 我最初嘗試了trusktr 的方法,但它觸發了奇怪的 eslint 和 IntelliJ IDEA 警告(他們聲稱該類沒有被聲明)。 以下解決方案很好,因為它消除了依賴循環。 沒有魔法。

  1. 將具有循環依賴關系的類分成兩部分:觸發循環的代碼和不觸發循環的代碼。
  2. 將不會觸發循環的代碼放入“內部”模塊中。 就我而言,我聲明了超類並刪除了所有引用子類的方法。
  3. 創建一個面向公眾的模塊。
    • import內部模塊。
    • import觸發依賴循環的模塊。
    • 重新添加我們在第 2 步中刪除的方法。
  4. 讓用戶導入面向公眾的模塊。

OP 的示例有點做作,因為在步驟 3 中添加構造函數比添加普通方法要困難得多,但一般概念保持不變。

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

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}

所有其他文件保持不變。

您可以通過動態加載模塊來解決它

我有同樣的問題,我只是動態導入模塊。

替換按需導入:

import module from 'module-path';

動態導入:

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

在您的示例中,您應該像這樣更改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}

有關動態導入的更多信息:

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

leo解釋了另一種方式,它僅適用於 ECMAScript 2019

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

為了分析循環依賴, Artur Hebda在這里解釋:

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