[英]How to fix this ES6 module circular dependency?
編輯:有關更多背景信息,另請參閱ES Discuss 上的討論。
我有三個模塊A
, B
和C
。 A
和B
進口從模塊的默認出口C
和模塊C
進口都默認A
和B
。 但是,模塊C
不依賴於在模塊評估期間從A
和B
導入的值,僅在運行時在所有三個模塊都已評估后的某個時間點。 模塊A
和B
確實依賴於在模塊評估期間從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
尚未評估。
通過制作這四個文件並運行入口點文件,您應該能夠輕松重現。
我的問題是(我可以有兩個具體的問題嗎?):為什么加載順序是這樣的? 如何編寫循環依賴的模塊,以便它們可以工作,以便在評估A
和B
時C
的值不會是undefined
?
(我認為 ES6 Module 環境可能能夠智能地發現它需要先執行模塊C
的主體,然后才能執行模塊A
和B
的主體。)
答案是使用“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
被評估。
這會導致模塊C
的var C
變量在class B extends C
定義之前被定義。 魔法!
需要注意的是,模塊C
必須使用var C
,而不是const
或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
調用initC
,就會拋出錯誤,並且模塊評估將失敗。
var
在模塊C
的范圍內提升,因此在initC
時可用。 這是一個很好的例子,說明了在 ES6+ 環境中您實際上想要使用var
而不是let
或const
的原因。
但是,您可以注意 rollup 沒有正確處理這個https://github.com/rollup/rollup/issues/845 ,並且看起來像let C = C
的黑客可以在某些環境中使用,如在上面鏈接到流星問題。
最后一件需要注意的重要事情是export default C
和export {C as default}
之間的區別。 第一個版本不會將模塊C
的C
變量作為實時綁定導出,而是按值導出。 因此,當使用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 C
和export {C as default}
之間的區別,而且在很多情況下他們不需要知道,但是了解跨模塊使用“實時綁定”時的區別很重要“init 函數”是為了解決循環依賴問題,以及實時綁定可能有用的其他問題。 不要鑽研太遠的話題,但是如果您有一個單例,則可以使用活動綁定作為使模塊范圍成為單例對象的一種方式,並且活動綁定是訪問單例中事物的方式。
描述實時綁定發生的事情的一種方法是編寫 javascript,其行為類似於上述模塊示例。 以下是模塊B
和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
和function initC
被提升到模塊之間,因此模塊B
能夠調用initC
然后立即使用C
,在var C
和function initC
之前function initC
在評估代碼中遇到。
當然,當模塊使用不同的標識符時會變得更復雜,例如,如果模塊B
import Blah from './c'
,那么Blah
仍然是到模塊C
的C
變量的實時綁定,但這不是很容易像前面的例子一樣描述使用普通變量提升,實際上Rollup 並不總是正確處理它。
例如,假設我們有如下的模塊B
,模塊A
和C
是相同的:
// --- Module B
import Blah, {initC} from './c';
initC();
console.log('Module B', Blah)
class B extends Blah {
// ...
}
export {B as default}
然后,如果我們使用純 JavaScript 僅描述模塊B
和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
和B
在模塊評估時依賴於C
,而不是在運行時。 評估模塊A
和B
,需要定義C
導出。 但是,在評估模塊C
時,它不依賴於定義的A
和B
導入。 模塊C
將來只需要在運行時使用A
和B
,在評估所有模塊之后,例如當入口點運行將運行C
構造函數的new A()
。 正是因為這個原因,模塊C
不需要initA
或initB
函數。
循環依賴中的多個模塊可能需要相互依賴,在這種情況下,需要一個更復雜的“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 警告(他們聲稱該類沒有被聲明)。 以下解決方案很好,因為它消除了依賴循環。 沒有魔法。
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}
所有其他文件保持不變。
您可以通過動態加載模塊來解決它
我有同樣的問題,我只是動態導入模塊。
替換按需導入:
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.