edit: more background, see discussion on es discuss.
i have 3 modules a
, b
, , c
. a
, b
import default export module c
, , module c
imports default both a
, b
. however, module c
not depend on values imported a
, b
during module evaluation, @ runtime @ point after 3 modules have been evaluated. modules a
, b
do depend on value imported c
during module evaluation.
the code looks this:
// --- module import c 'c' class extends c { // ... } export {a default}
.
// --- module b import c 'c' class b extends c { // ... } export {b default}
.
// --- module c import 'a' import b 'b' class c { constructor() { // may run later, after 3 modules evaluated, or // possibly never. console.log(a) console.log(b) } } export {c default}
i have following entry point:
// --- entrypoint import './app/a' console.log('entrypoint', a)
but, happens module b
evaluated first, , fails error in chrome (using native es6 classes, not transpiling):
uncaught typeerror: class extends value undefined not function or null
what means value of c
in module b
when module b
being evaluated undefined
because module c
has not yet been evaluated.
you should able reproduce making 4 files, , running entrypoint file.
my questions (can have 2 concrete questions?): why load order way? how can circularly-dependent modules written work value of c
when evaluating a
, b
not undefined
?
(i think es6 module environment may able intelligently discover need execute body of module c
before can possibly execute bodies of modules a
, b
.)
the answer use "init functions". reference, @ 2 messages starting here: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21
the solution looks this:
// --- module import c, {initc} './c'; initc(); console.log('module a', c) class extends c { // ... } export {a default}
-
// --- module b import c, {initc} './c'; initc(); console.log('module b', c) class b extends c { // ... } export {b default}
-
// --- module c import './a' import b './b' var c; export function initc(){ if (c) return; c = class c { constructor() { console.log(a) console.log(b) } } } initc(); export {c default}; // important: not `export default c;` !!
-
// --- entrypoint import './a' console.log('entrypoint', new a) // runs console.logs in c constructor.
also see thread related info: https://github.com/meteor/meteor/issues/7621#issuecomment-238992688
it important note exports hoisted (it may strange, can ask in esdiscuss learn more) var
, hoisting happens across modules. classes cannot hoisted, functions can (just in normal pre-es6 scopes, across modules because exports live bindings reach other modules possibly before evaluated, if there scope encompasses modules identifiers can accessed through use of import
).
in example, entry point imports module a
, imports module c
, imports module b
. means module b
evaluated before module c
, due fact exported initc
function module c
hoisted, module b
given reference hoisted initc
function, , therefore module b
call call initc
before module c
evaluated.
this causes var c
variable of module c
become defined prior class b extends c
definition. magic!
it important note module c
must use var c
, not const
or let
, otherwise temporal deadzone error should theoretically thrown in true es6 environment. example, if module c looked like
// --- module c import './a' import b './b' let c; export function initc(){ if (c) return; c = class c { constructor() { console.log(a) console.log(b) } } } initc(); export {c default}; // important: not `export default c;` !!
then module b
calls initc
, error thrown, , module evaluation fail.
var
hoisted within scope of module c
, available when initc
called. great example of reason why you'd want use var
instead of let
or const
in es6+ environment.
however, can take note rollup doesn't handle correctly https://github.com/rollup/rollup/issues/845, , hack looks let c = c
can used in environments pointed out in above link meteor issue.
one last important thing note difference between export default c
, export {c default}
. first version does not export c
variable module c
live binding, value. so, when export default c
used, value of var c
undefined
, assigned onto new variable var default
hidden inside es6 module scope, , due fact c
assigned onto default
(as in var default = c
value, whenever default export of module c
accessed module (for example module b
) other module reaching module c
, accessing value of default
variable going undefined
. if module c
uses export default c
, if module b
calls initc
(which does change values of module c
's internal c
variable), module b
won't accessing internal c
variable, accessing default
variable, still undefined
.
however, when module c
uses form export {c default}
, es6 module system uses c
variable default exported variable rather making new internal default
variable. means c
variable live binding. time module depending on module c
evaluated, given module c
's internal c
variable @ given moment, not value, handing on variable other module. so, when module b
calls initc
, module c
's internal c
variable gets modified, , module b
able use because has reference same variable (even if local identifier different)! basically, time during module evaluation, when module use identifier imported module, module system reaches other module , gets value @ moment in time.
i bet people won't know difference between export default c
, export {c default}
, , in many cases won't need to, important know difference when using "live bindings" across modules "init functions" in order solve circular dependencies, among other things live bindings can useful. not delve far off topic, if have singleton, alive bindings can used way make module scope singleton object, , live bindings way in things singleton accessed.
one way describe happening live bindings write javascript behave similar above module example. here's modules b
, c
might in way describes "live bindings":
// --- 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()
this shows happening in in es6 module version: b evaluated first, var c
, function initc
hoisted across modules, module b
able call initc
, use c
right away, before var c
, function initc
encountered in evaluated code.
of course, gets more complicated when modules use differing identifiers, example if module b
has import blah './c'
, blah
still live binding c
variable of module c
, not easy describe using normal variable hoisting in previous example, , in fact rollup isn't handling properly.
suppose example have module b
following , modules a
, c
same:
// --- module b import blah, {initc} './c'; initc(); console.log('module b', blah) class b extends blah { // ... } export {b default}
then if use plain javascript describe happens modules b
, c
, result this:
// --- module b initc() console.log('module b', blah) class b extends blah { // ... } // --- module c var c var blah // needs added function initc() { if (c) return c = class c { constructor() { console.log(a) console.log(b) } } blah = c // needs added } initc()
another thing note module c
has initc
function call. in case module c
ever evaluated first, won't hurt initialize then.
and last thing note in these example, modules a
, b
depend on c
at module evaluation time, not @ runtime. when modules a
, b
evaluated, require c
export defined. however, when module c
evaluated, not depend on a
, b
imports being defined. module c
need use a
, b
@ runtime in future, after modules evaluated, example when entry point runs new a()
run c
constructor. reason module c
not need inita
or initb
functions.
it possible more 1 module in circular dependency need depend on each other, , in case more complex "init function" solution needed. example, suppose module c
wants console.log(a)
during module evaluation time before class c
defined:
// --- module c import './a' import b './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 default}; // important: not `export default c;` !!
due fact entry point in top example imports a
, c
module evaluated before a
module. means console.log(a)
statement @ top of module c
log undefined
because class a
hasn't been defined yet.
finally, make new example work logs class a
instead of undefined
, whole example becomes more complicated (i've left out module b , entry point, since don't change):
// --- module import c, {initc} './c'; initc(); console.log('module a', c) var export function inita() { if (a) return initc() = class extends c { // ... } } inita() export {a default} // important: not `export default a;` !!
-
// --- module c import a, {inita} './a' import b './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 default}; // important: not `export default c;` !!
now, if module b
wanted use a
during evaluation time, things more complicated, leave solution imagine...
Comments
Post a Comment