編集:より多くの背景については、 ESの議論に関する議論 も参照してください。
A
、B
、C
の3つのモジュールがあります。 A
およびB
は、モジュールC
からデフォルトのエクスポートをインポートし、モジュールC
は、A
およびB
の両方からデフォルトをインポートします。ただし、モジュールC
は、モジュール評価中にA
およびB
からインポートされた値に依存せず、3つのモジュールすべてが評価された後のある時点でのみ実行されます。モジュールA
および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
になります。
これらの4つのファイルを作成し、エントリポイントファイルを実行することで、簡単に再現できるはずです。
私の質問は次のとおりです(具体的な質問が2つありますか?):読み込み順序がそのようになっているのはなぜですか? C
およびA
を評価するときのB
の値がundefined
にならないように循環依存モジュールをどのように書くことができますか?
(ES6モジュール環境は、モジュールC
およびA
の本体を実行する前に、モジュールB
の本体を実行する必要があることをインテリジェントに検出できると思います。)
制御の反転を使用することをお勧めします。次のような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/-KlDeP9Rb60MehsCMa8
このコメントへの応答として更新します: この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モジュールの循環依存関係を修正する方法?
エンドユーザーがクラスのサブセットをインポートできるようにするには、lib.jsファイルを作成して、公開APIをエクスポートします。
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)
はい、それは嫌なハックですが、動作します
動的にモジュールをロードすることで解決できます
同じ問題があり、モジュールを動的にインポートするだけです。
オンデマンドインポートの置換:
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/
ここに私のために働いた簡単な解決策があります。私は最初に trusktrのアプローチ を試しましたが、奇妙なeslintとIntelliJ IDEA=警告(クラスは宣言されていないと宣言されたと主張しました)。依存関係のループを排除します。
import
最初に内部モジュール。import
依存関係ループをトリガーしたモジュール。OPの例は、手順3でコンストラクターを追加するのが通常のメソッドを追加するよりもはるかに難しいため、少し工夫されていますが、一般的な概念は同じままです。
// 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}
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}
他のすべてのファイルは変更されません。