How to solve this basic ES6-module circular dependency problem?
Logan Smyth
loganfsmyth at gmail.com
Tue Aug 16 17:48:40 UTC 2016
> Your `initC` solution is working in Meteor (Babel + Reify) and
Webpack+Babel, but the `initC` logic seems to run twice, as if there are
two C variables instead of one. The following code is based on yours, and
the `console.log('initC!!!')` statement unexpectedly executes twice, and
you'll see output like this:
Damn, that's a Babel bug with the block scoping logic. That said, as in my
example, that needs to be `var C;` anyway, `let` would throw (in an
environment with working TDZ anyway). Changing it to `var` also stops the
duplicate printing.
> I'm also not sure how `initC` can be defined when it is called in the B
module, which is evaluated before C and A. The evaluation order is B, C, A
(depth first). Does the `initC` function get hoisted into a scope common
with all three modules? That is the only way that would seem to explain it,
but that seems to go against the intuition I had that each module had it's
own module scope (as if it were wrapped inside a `function() {}`, and
therefore I thought the `initC` function would be hoisted within the C
module, and that with B being evaluated first I thought an "undefined"
error would be thrown when it tried to execute `initC` (but that is not
happening!). How is it that `initC` can be available to the B module before
C is evaluated?
There are two separate pieces to executing a module, `Instantiation`, and
`Evaluation`, which are what comes into play here. When you tell a JS
environment to execute a file, it will instantiate every ES6 module in
dependency graph before beginning to execute _any_ of the modules. Babel
does its best to simulate this behavior, though it's not perfect at it. One
of the things that happens during module instantiation is that hoisted
declarations are initialized, which means you can import and call a
function declaration from any module, even if that module hasn't started
the `Evaluation` phase yet, the same way it'd work with other cases of
hoisting, where execution hasn't reached the function declaration, but it
is available early.
This behavior is why you can't use `let C` there, because when `B` is being
evaluated, the `let C` line won't have run because the Evaluation phase of
`C` hasn't started yet. You can however access the `initC` function because
it is a function declaration. As long as there are no TDZ errors in what
you're doing, that function can do whatever it would like, assuming it
doesn't depend on other stuff that would require `Evaluation` to have
finished in `C`, the same as what happens with hoisting normally. That
means for instance you couldn't do
import B from './B';
var SOME_CONSTANT = "hello";
export function initC(){
return SOME_CONSTANT;
}
because calling `initC` here would return `undefined` if called from inside
a dependency cycle from `B`.
On Sat, Aug 13, 2016 at 9:09 PM, /#!/JoePea <joe at trusktr.io> wrote:
> Hi Logan,
>
> > The example I posted works properly with Babel's live-binding
> implementation and should require less repetition. What were your thoughts
> on it?
>
> Your `initC` solution is working in Meteor (Babel + Reify) and
> Webpack+Babel, but the `initC` logic seems to run twice, as if there are
> two C variables instead of one. The following code is based on yours, and
> the `console.log('initC!!!')` statement unexpectedly executes twice, and
> you'll see output like this:
>
> ```
> module B
> initC!!!
> module C
> initC!!!
> module A
> function A() { ... }
> function B() { ... }
> Entrypoint A {}
> ```
>
> I'm also not sure how `initC` can be defined when it is called in the B
> module, which is evaluated before C and A. The evaluation order is B, C, A
> (depth first). Does the `initC` function get hoisted into a scope common
> with all three modules? That is the only way that would seem to explain it,
> but that seems to go against the intuition I had that each module had it's
> own module scope (as if it were wrapped inside a `function() {}`, and
> therefore I thought the `initC` function would be hoisted within the C
> module, and that with B being evaluated first I thought an "undefined"
> error would be thrown when it tried to execute `initC` (but that is not
> happening!). How is it that `initC` can be available to the B module before
> C is evaluated?
>
> This is the code I have:
>
> ```js
> // --- Entrypoint
> import A from './A'
> console.log('Entrypoint', new A)
> ```
>
> ```js
> // --- Module A
>
> import C, {initC} from './C'
>
> console.log('module A')
> initC()
>
> class A extends C {
> // ...
> }
>
> export {A as default}
> ```
>
> ```js
> // --- Module B
>
> import C, {initC} from './C'
>
> console.log('module B')
> initC()
>
> class B extends C {
> // ...
> }
>
> export {B as default}
> ```
>
> ```js
> // --- Module C
>
> import A from './A'
> import B from './B'
>
> console.log('module C')
> let C
>
> export function initC(){
> if (C) return
>
> console.log('initC!!!')
>
> C = class C {
> constructor() {
> // this may run later, after all three modules are evaluated,
> or
> // possibly never.
> console.log(A)
> console.log(B)
> }
> }
> }
>
> initC()
>
> export {C as default}
> ```
>
> */#!/*JoePea
>
> On Thu, Aug 11, 2016 at 10:26 AM, Logan Smyth <loganfsmyth at gmail.com>
> wrote:
>
>> Keep in mind `let A = A;` is a TDZ error in any real ES6 environment.
>>
>> The example I posted works properly with Babel's live-binding
>> implementation and should require less repetition. What were your thoughts
>> on it?
>>
>> On Thu, Aug 11, 2016 at 12:23 AM, /#!/JoePea <joe at trusktr.io> wrote:
>>
>>> Alright, so I believe I have found the solution. It is not possible to
>>> guarantee a certain module evaluation order, but using some clever (but
>>> tedious) conditional checking I believe the problem is solved (with two
>>> caveats listed after):
>>>
>>> ```js
>>> // --- Entrypoint
>>> import A from './A'
>>> console.log('Entrypoint', new A)
>>> ```
>>>
>>> ```js
>>> // --- Module A
>>>
>>> import C from './C'
>>> import {setUpB} from './B'
>>>
>>> let A
>>>
>>> export
>>> function setUpA(C) {
>>>
>>> if (!A) {
>>> A = class A extends C {
>>> // ...
>>> }
>>> }
>>>
>>> }
>>>
>>> if (setUpA && C) setUpA(C)
>>> if (setUpB && C) setUpB(C)
>>>
>>> export {A as default}
>>> ```
>>>
>>> ```js
>>> // --- Module B
>>>
>>> import C from './C'
>>> import {setUpA} from './A'
>>>
>>> let B
>>>
>>> export
>>> function setUpB(C) {
>>>
>>> if (!B) {
>>> B = class B extends C {
>>> // ...
>>> }
>>> }
>>>
>>> }
>>>
>>> if (setUpA && C) setUpA(C)
>>> if (setUpB && C) setUpB(C)
>>>
>>> export {B as default}
>>> ```
>>>
>>> ```js
>>> // --- Module C
>>>
>>> import A, {setUpA} from './A'
>>> import B, {setUpB} from './B'
>>>
>>> class C {
>>> constructor() {
>>> // this may run later, after all three modules are evaluated, or
>>> // possibly never.
>>> console.log(A)
>>> console.log(B)
>>> }
>>> }
>>>
>>> if (setUpA && C) setUpA(C)
>>> if (setUpB && C) setUpB(C)
>>>
>>> export {C as default}
>>> ```
>>>
>>> The caveat is that this fails in both Babel environments and in Rollup.
>>> For it to work in Babel environments, `let A` and `let B` have to be
>>> changed to `let A = A` and `let B = B`, as per the [fault in Babel's
>>> ES2015-to-CommonJS implementation](https://github
>>> .com/meteor/meteor/issues/7621#issuecomment-238992688) pointed out by
>>> Ben Newman. And it fails in Rollup because Rollup [isn't really creating
>>> live bindings](https://github.com/rollup/rollup/issues/845), which is
>>> fine in most cases, but doesn't work with these circular dependencies. The
>>> Rollup output does not create the C reference before it is ever used in the
>>> first pair of conditional checks, unlike what (I think) would happen with
>>> real live bindings (please correct me if wrong). To understand what I mean
>>> in the case of Rollup, just run `if (FOO) console.log(FOO)` in your
>>> console, and you'll get an error because FOO is not defined. Had the
>>> bindings been live, then FOO *would* be defined.
>>>
>>>
>>>
>>> */#!/*JoePea
>>>
>>> On Wed, Aug 10, 2016 at 5:04 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>
>>>> I found a solution that works in environments compiled by Babel, using
>>>> the [workaround suggested by Ben Newman](https://github.com/met
>>>> eor/meteor/issues/7621#issuecomment-238992688):
>>>>
>>>> ```js
>>>> // --- Module A
>>>>
>>>> import C from './C'
>>>>
>>>> let A = A // @benjamn's workaround applied
>>>>
>>>> export
>>>> function setUpA(C) {
>>>>
>>>> A = class A extends C {
>>>> // ...
>>>> }
>>>>
>>>> }
>>>>
>>>> export {A as default}
>>>> ```
>>>>
>>>> ```js
>>>> // --- Module B
>>>>
>>>> import C from './C'
>>>>
>>>> let B = B // @benjamn's workaround applied
>>>>
>>>> export
>>>> function setUpB(C) {
>>>>
>>>> B = class B extends C {
>>>> // ...
>>>> }
>>>>
>>>> }
>>>>
>>>> export {B as default}
>>>> ```
>>>>
>>>> ```js
>>>> // --- Module C
>>>>
>>>> import A, {setUpA} from './A'
>>>> import B, {setUpB} from './B'
>>>>
>>>> let C = class C {
>>>> constructor() {
>>>> // this may run later, after all three modules are evaluated, or
>>>> // possibly never.
>>>> console.log(A)
>>>> console.log(B)
>>>> }
>>>> }
>>>>
>>>> setUpA(C)
>>>> setUpB(C)
>>>>
>>>> export {C as default}
>>>> ```
>>>>
>>>> ```js
>>>> // --- Entrypoint
>>>>
>>>> import A from './A'
>>>> console.log('Entrypoint', new A) // runs the console.logs in the C
>>>> constructor.
>>>> ```
>>>>
>>>>
>>>> Although that works in my environment which is compiled from ES6
>>>> modules to CommonJS by Babel, it [doesn't work in Rollup.js](
>>>> http://goo.gl/PXXBKI), and may not work in other ES6 module
>>>> implementations.
>>>>
>>>> Is there some solution that will theoretically work in any ES6 module
>>>> environment?
>>>>
>>>> */#!/*JoePea
>>>>
>>>
>>>
>>> _______________________________________________
>>> es-discuss mailing list
>>> es-discuss at mozilla.org
>>> https://mail.mozilla.org/listinfo/es-discuss
>>>
>>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160816/ec5e353c/attachment-0001.html>
More information about the es-discuss
mailing list