Nested import declarations S0
中文标题:嵌套导入声明
- 阶段: Stage 0
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
js 中运行。它引入了一个运行时 API,包括 module.link、module.export 和 module.runSetters 等方法,以实现实时绑定并支持嵌套导入。该项目在 README 中有构建徽章,表明正在积极维护,但未提及在 TC39 流程中的具体阶段。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
re·i·fy 动词,及物 
re·i·fied 过去式 re·i·fies 现在式 re·i·fy·ing 分词 re·i·fi·ca·tion 名词 re·i·fi·er 名词
- 使(抽象的事物)更具体或更真实
"这些本能,在人类身上,被具体化为言语结构" - 将(想法、概念等)视为或当作具有物质存在
- 使得 ECMAScript 2015 模块 可以在 任意 版本的 Node.js 中运行
用法
- 在您的包或应用目录中运行
npm install --save reify。--save很重要,因为具体化仅适用于显式依赖reify包的模块。 - 在导入包含
import和export声明的模块之前,调用require("reify")。
您还可以轻松地 reify Node REPL:
工作原理
由 reify 编译器生成的代码依赖于一个简单的运行时 API,可以通过一系列示例来解释。虽然您不必手动编写这个 API,但它被设计为易于人类阅读和编写,部分原因是为了更容易解释。
我将首先解释 Module.prototype.link 方法,然后再解释 Module.prototype.export 方法。请注意,这个 Module 是 CommonJS module 对象的构造函数,而 import 和 export 方法是对 Module.prototype 的自定义添加。
module.link(id, setters)
我们开始:
变成
所有 setter 函数在 module.link 返回之前同步调用,并带有当前可用的任何值。但是,当存在导入循环时,某些 setter 函数可能会在导出值更改时再次被调用。调用这些 setter 函数一次或多次是实现 实时绑定 的关键,这是 ECMAScript 2015 规范所要求的。
导入命名空间对象与导入命名导出没有区别。名称仅仅是 "*" 而不是合法的标识符:
变成
请注意,这里暴露的 ns 对象是 !== require("./utils"),而是 require("./utils") 对象的规范化视图。这种方法确保实际的 exports 对象永远不会暴露给 module.link 的调用者。
请注意,这种编译策略无论 import 声明出现在何处都同样有效:
变成
参见 WHY_NEST_IMPORTS.md 获取关于为什么嵌套 import 声明值得的更详细讨论。
module.export(getters)
那 export 声明呢?一种选择是将它们转换为更新 exports 对象的 CommonJS 代码,因为与 Node 和 CommonJS 的互操作性当然是这种方法的目标。
然而,如果 Module.prototype.link 接受一个 id 字符串和一个 setter 函数映射,那么 Module.prototype.export 自然应该是一个注册 getter 函数的方法。有了这些 getter 函数,每当父模块调用 module.link(id, ...) 时,id 模块的 getter 就会运行,更新其 module.exports 对象,从而使 module.link 方法能够访问最新的导出值。
module.export 方法使用一个单独的对象字面量调用,其键是导出的符号名称,其值是这些导出符号的 getter 函数。例如,
变成
这段代码注册了变量 a、b... 的 getter 函数,以便 module.link 可以随时轻松地获取这些变量的最新值。重要的是我们注册 getter 函数而不是存储计算值,这样其他模块总能导入最新的值。
导出重映射也有效:
变成
请注意,module.export 调用被“提升”到它出现的块的顶部。这是安全的,因为 getter 函数在导出变量声明的作用域内的任何位置都同样有效,而且这是一个好主意,因为提升确保了 getter 尽可能早地注册。
那 export default <expression> 声明呢?延迟评估 default 表达式是错误的,所以将其包装在提升的 getter 函数中并不完全是我们想要的。
相反,
被原地替换(没有任何提升)为
module.exportDefault 方法只是 module.export 的一个方便的包装器:
我们传递给 module.export 的 true 参数是一个提示,表明这个 getter 函数返回的值永远不会改变,这启用了一些优化。
module.runSetters()
现在,假设您在模块加载完成后更改了导出的局部变量的值。那么您需要让模块系统知道这个更新,这就是 module.runSetters 的作用。每当模块完成加载时,模块系统代表您调用此方法,但您也可以手动调用它,或者简单地让 reify 在您赋值给导出的局部变量时生成调用 module.runSetters 的代码。
无参数调用 module.runSetters() 会导致任何依赖当前模块的 setter 重新运行,但仅当 setter 将接收的值与上次传递给 setter 的值不同时。
如果您向 module.runSetters 传递一个参数,该参数的值将原样返回,以便您可以轻松地将赋值表达式包装在 module.runSetters 调用中:
应该变成
请注意,module.runSetters(argument) 实际上并不使用 argument。然而,通过让 module.runSetters(argument) 原样返回 argument,我们可以在赋值后立即运行 setter,而不会干扰更大表达式的求值。
因为 module.runSetters 会运行任何具有新值的 setter,所以它对于难以静态分析的可能有风险的表达式也很有用:
实为 import 的 export
那 export ... from "./module" 声明呢?这里的关键见解是 带有 from "..." 子句的 export 声明实际上只是更新 exports 对象而不是更新局部变量的 import 声明:
变成
由于这种模式非常普遍,并且这些 setter 函数不需要修改局部变量,运行时 API 支持一种替代的简写方式来重新导出值:
这种策略可以干净地推广到 export * from "..." 声明:
变成
虽然基本原则相同,但实际上 Reify 编译器也会为这种模式生成简写符号:
这个版本更短,不依赖于 Object.assign(或 polyfill),在复制 getter 等特殊属性时可以更智能一点,并且可靠地修改 module.exports 而不是 exports 变量(无论它是什么)。胜利!
导出命名命名空间(提案):
变成
简写:
重新导出默认导出(提案):
变成
简写:
虽然这些示例没有涵盖 import 和 export 声明的每一种可能的语法,但我希望它们提供了必要的直觉,让您能想象任何声明都能被编译。
当我有时间时,我希望实现一个实时编译文本编辑器以便进行实验。