tru*_*ktr 35 javascript module es6-module-loader
编辑:有关更多背景,请参阅ES讨论的讨论.
我有三个模块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}
Run Code Online (Sandbox Code Playgroud)
.
// --- Module B
import C from 'C'
class B extends C {
// ...
}
export {B as default}
Run Code Online (Sandbox Code Playgroud)
.
// --- 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}
Run Code Online (Sandbox Code Playgroud)
我有以下切入点:
// --- Entrypoint
import A from './app/A'
console.log('Entrypoint', A)
Run Code Online (Sandbox Code Playgroud)
但是,实际发生的事情是B首先评估模块,并且它在Chrome中出现此错误(使用本机ES6类,而不是转换):
Uncaught TypeError: Class extends value undefined is not a function or null
Run Code Online (Sandbox Code Playgroud)
这意味着C在评估模块B时模块B中的值是undefined因为模块C尚未被评估.
您应该能够通过制作这四个文件并运行入口点文件来轻松地重现.
我的问题是(我可以提出两个具体问题吗?):为什么负载顺序是这样的?如何能圆依赖的模块被写入,使他们的工作,这样的价值C评估时A,并B不会undefined?
(我倒觉得ES6模块环境可能能够智能地发现它需要执行模块的身体C,才可以可能执行模块的机构A和B.)
tru*_*ktr 14
答案是使用"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}
Run Code Online (Sandbox Code Playgroud)
-
// --- Module B
import C, {initC} from './c';
initC();
console.log('Module B', C)
class B extends C {
// ...
}
export {B as default}
Run Code Online (Sandbox Code Playgroud)
-
// --- 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;` !!
Run Code Online (Sandbox Code Playgroud)
-
// --- Entrypoint
import A from './A'
console.log('Entrypoint', new A) // runs the console.logs in the C
constructor.
Run Code Online (Sandbox Code Playgroud)
另请参阅此主题以获取相关信息:https://github.com/meteor/meteor/issues/7621#issuecomment-238992688
重要的是要注意出口是悬挂的(可能很奇怪,你可以在es escucuss中要求了解更多)就像var,但是吊装发生在模块之间.类不能被提升,但是函数可以是(就像它们在正常的ES6之前的范围中,但是在模块之间,因为导出是实时绑定,可能在它们被评估之前到达其他模块,几乎就像有一个范围包含所有模块只能通过使用来访问标识符import.
在此示例中,入口点从模块A导入,模块从模块导入,从模块C导入B.这意味着模块B将在模块之前进行评估C,但由于模块的导出initC函数C被提升,模块B将被赋予对此提升initC函数的引用,因此在模块评估之前模块B调用调用.initCC
这会导致var C模块的变量在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;` !!
Run Code Online (Sandbox Code Playgroud)
然后,一旦模块B调用initC,就会抛出错误,模块评估将失败.
var在模块范围内被提升C,因此可以在initC调用时使用.这是一个很好的例子,说明您实际上想要使用var而不是在ES6 +环境中使用let或const在ES6 +环境中使用.
但是,你可以注意汇总不能正确处理这个https://github.com/rollup/rollup/issues/845,并且看起来像一个hack let C = C可以在某些环境中使用,如上面链接指出的那样流星问题.
一个需要注意的最后一个重要的事情是之间的差异export default C和export {C as default}.第一个版本不会将C模块中的变量C作为实时绑定导出,而是按值导出.所以,当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) s内部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},而且在许多情况下,他们将不再需要,但为了解决圆形使用"实时绑定"时,跨模块与"初始化函数"来知道其中的差别是非常重要的依赖性,以及活动绑定可能有用的其他内容.不要过分偏离主题,但如果你有一个单例,活动绑定可以用作使模块范围成为单例对象的方法,并且实时绑定从单例访问事物的方式.
描述实时绑定发生的事情的一种方法是编写与上述模块示例类似的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()
Run Code Online (Sandbox Code Playgroud)
这表明有效什么是在ES6模块版本发生在:B先进行评估,但var C和function initC整个模块悬挂,所以模块B是能够调用initC,然后使用C向右走,之前var C并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}
Run Code Online (Sandbox Code Playgroud)
然后,如果我们用普通的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()
Run Code Online (Sandbox Code Playgroud)
另外需要注意的是模块C也有initC函数调用.这是以防万一C首先评估模块,然后初始化它不会有害.
而且要注意的最后一件事是,在这些例子中,模块A和B依赖C 于模块评估时间,不能在运行时.当模块A和B被评估,则要求对C来定义导出.然而,当模块C进行评估,它不依赖于A与B所定义的进口.模块C只需要使用A并且B在将来运行时,所有模块进行评估之后,例如,当入口点运行new A(),这将运行C构造函数.因此,模块C不需要initA或initB不起作用.
循环依赖中的多个模块可能需要彼此依赖,并且在这种情况下需要更复杂的"初始化函数"解决方案.例如,假设模块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;` !!
Run Code Online (Sandbox Code Playgroud)
由于顶部示例中的入口点导入A,因此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;` !!
Run Code Online (Sandbox Code Playgroud)
-
// --- 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;` !!
Run Code Online (Sandbox Code Playgroud)
现在,如果模块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;
Run Code Online (Sandbox Code Playgroud)
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); }
Run Code Online (Sandbox Code Playgroud)
或使用此模式:
// --- 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;
Run Code Online (Sandbox Code Playgroud)
更新,响应此评论:如何修复此 ES6 模块循环依赖?
要允许最终用户导入类的任何子集,只需创建一个导出面向公众的 api 的 lib.js 文件:
import A from "./A";
import B from "./B";
import C from "./C";
export { A, B, C };
Run Code Online (Sandbox Code Playgroud)
或者:
import A from "./A";
import B from "./B";
import C from "./ConcreteCImplementation";
export { A, B, C };
Run Code Online (Sandbox Code Playgroud)
然后你可以:
// --- 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();
Run Code Online (Sandbox Code Playgroud)
更新:我将把这个答案留给后代,但现在我使用这个解决方案。
这是一个对我有用的简单解决方案。我最初尝试了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}
Run Code Online (Sandbox Code Playgroud)
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}
Run Code Online (Sandbox Code Playgroud)
所有其他文件保持不变。