ES6 命名导出异步变更后为何能被导入方同步感知?
核心原理:ESM 的活绑定机制
你观察到的所有差异,本质源于 ES 模块(ESM)的活绑定(Live Binding)设计,以及你对 import { member } 语法的错误认知——它不是 import * as 再解构的语法糖,二者底层逻辑完全不同。
首先明确两个基础规则:
- 所有 ESM 的导出本质都是绑定的暴露,而非值的拷贝
- 命名导出和默认导出的语法差异,决定了绑定指向的对象不同
1. 命名导入为什么能拿到更新后的值?
当你写 export let member = "initial" 时,属于声明式命名导出:你导出的不是 member 变量当前的值,而是 member 这个变量本身的引用(绑定)。
而 import { member } from "./module.mjs" 是直接把导入方的 member 标识符,指向导出方模块内部的 member 变量地址。二者本质是同一个变量的两个别名,所以导出方修改 member 的值,导入方访问时自然能拿到最新值。
2. import * as allExports 为什么也能拿到更新值?
import * as 得到的是模块命名空间对象,这是一个特殊的只读对象,它的每一个属性都是对应命名导出的 getter 访问器。当你调用 allExports.member 时,实际上是通过 getter 去访问原模块 member 变量的活绑定,所以也能拿到最新值。
3. 默认导出为什么拿不到更新值?
这是默认导出的语法规则决定的:export default 后面只能跟表达式,不能跟变量声明。你写 export default member 时,等价于把 member 变量当前的求值结果,绑定到模块的 default 导出上。
也就是说,此时 default 导出的绑定指向的是 "initial" 这个字符串值,而不是原模块的 member 变量。后续你修改 member 变量,和 default 导出的绑定没有任何关系,所以导入方拿到的永远是初始的 "initial"。
如果想让默认导出也支持活绑定,你可以导出一个包含 getter 的对象,或者导出函数返回最新值:
// module.mjs 活绑定默认导出写法 let member = "initial" export default { get value() { return member } } setTimeout(() => member = "mutated", 2000) // index.mjs import m from "./module.mjs" setInterval(() => console.log(m.value), 300) // 2秒后会输出 mutated
4. import * as 再解构为什么拿不到更新值?
这里的解构是普通的 JavaScript 对象解构,和 ESM 语法没有关系。你写 let { member } = allExports 时,等价于在解构执行的那一刻,执行了 let member = allExports.member——也就是把 allExports.member 当时的取值("initial")赋值给了本地的 member 变量。
之后你访问的是本地的普通变量 member,和模块命名空间对象的 getter 已经没有关联,自然拿不到后续更新的值。
内容的提问来源于stack exchange,提问作者Niklas Gruhn

