toDynamicValue 异步支持分析
背景
本库的 @PostConstruct 异步等待机制目前只支持 BINDING.INSTANCE 类型的服务。具体来说,_postConstruct 方法中(src/binding.ts 第 181 行附近)的过滤逻辑:
const bindings = propertyBindings.filter(
item => BINDING.INSTANCE === item?.type
);这意味着当一个服务通过 @PostConstruct(true) 声明需要等待前置依赖初始化完成时,只会等待那些通过 to() / toSelf() 绑定的类实例服务,而不会等待 toConstantValue 和 toDynamicValue 绑定的服务。
当前现状分析
toConstantValue —— 无需支持异步
toConstantValue(value) 接收的是一个已经确定的值,在绑定时就已经传入,不涉及任何实例化过程,也没有生命周期钩子。常量天然是同步的,不存在异步初始化的需求。
用户当然可以写 toConstantValue(somePromise),但此时注入的是 Promise 对象本身,而不是 Promise 的结果,这是用户的主动选择,不属于框架需要处理的异步初始化问题。
结论:toConstantValue 可以忽略,无需支持异步。
toDynamicValue —— 存在异步需求
toDynamicValue 的类型签名当前是同步的:
export type DynamicValue<T> = (ctx: Context) => T;_resolveDynamicValue 也是同步执行:
_resolveDynamicValue() {
this.status = STATUS.INITING;
const dynamicValue = this.dynamicValue!(this.context);
this.cache = this.activate(dynamicValue);
this.status = STATUS.ACTIVATED;
return this.cache;
}如果用户传入 async 函数,this.cache 存储的是 Promise 对象本身,而不是 resolve 后的值。
但在实际场景中,toDynamicValue 返回异步值是合理的需求,例如:
- 数据库连接初始化
- 远程配置加载
- 需要异步创建的第三方 SDK 客户端
本库已经提供了 container.getAsync() 方法,具备处理异步的基础设施,因此扩展 toDynamicValue 支持异步是可行的方向。
_postConstruct 只等待 INSTANCE 的原因
只有 BINDING.INSTANCE 类型的绑定才会执行 _postConstruct 生命周期,CONSTANT 和 DYNAMIC 类型的绑定:
- 没有类构造过程
- 没有
@PostConstruct装饰器 postConstructResult始终是初始值UNINITIALIZED
所以当前等待它们的 postConstructResult 没有意义。要支持 DYNAMIC 的异步,需要引入新的异步标记机制。
其他开源库的实现方案
InversifyJS
InversifyJS 将异步解析拆分为两条独立路径:
- 同步路径:
container.get()始终同步返回。如果toDynamicValue传入 async 函数,get()返回的是 Promise 对象本身。 - 异步路径:
container.getAsync()会 await 异步结果。toDynamicValue可以传入 async 函数,但必须通过getAsync来解析。 - Provider 模式:提供
toProvider绑定类型,本质上注入的是一个返回 Promise 的工厂函数,调用方自行 await。
InversifyJS 的 onActivation 也支持返回 T | Promise<T>,在 getAsync 路径下会被 await。
NestJS
NestJS 的异步 provider 通过 useFactory 实现:
useFactory可以返回 Promise,框架会在应用启动阶段统一 await 所有异步 provider。useValue对应常量,不支持异步。- 异步 provider 的解析发生在模块初始化阶段,而非运行时按需解析。
这与本库的按需解析(lazy resolution)模式有本质区别。NestJS 的方案更适合启动时一次性初始化的场景。
tsyringe(微软)
tsyringe 不原生支持异步解析,社区有 @launchtray/tsyringe-async 这样的 fork 来补充异步能力。
小结
| 框架 | 异步 DynamicValue | 实现方式 |
|---|---|---|
| InversifyJS | ✅ 支持 | get / getAsync 双路径 |
| NestJS | ✅ 支持 | useFactory + 启动阶段统一 await |
| tsyringe | ❌ 不支持 | 需要社区 fork |
| 本库 | ❌ 暂不支持 | 已有 getAsync 基础设施 |
实现方案
核心思路
本库已有 getAsync 方法,可以参考 InversifyJS 的双路径模式。核心改动是让 toDynamicValue 的异步结果能够被 @PostConstruct(true) 的等待机制感知到。
需要修改的点
1. 类型签名扩展
DynamicValue<T> 的返回类型需要兼容 Promise:
(ctx: Context) => T → (ctx: Context) => T | Promise<T>2. Binding 增加通用异步标记
当前只有 postConstructResult 用于标记 INSTANCE 类型的异步状态。需要引入一个更通用的异步标记字段(例如 asyncResult),用于 DYNAMIC 类型存储其异步解析的 Promise。
3. _resolveDynamicValue 检测异步返回值
_resolveDynamicValue 需要检测 dynamicValue 函数的返回值是否是 Promise:
- 如果是同步值:行为不变,直接存入 cache
- 如果是 Promise:将 Promise 存入异步标记字段,Promise resolve 后再更新 cache
4. _postConstruct 等待逻辑扩展
_postConstruct 中收集需要等待的 Promise 时,不再只看 BINDING.INSTANCE 的 postConstructResult,还需要收集 BINDING.DYNAMIC 的异步标记。
5. getAsync 适配
getAsync 当前只 await postConstructResult,需要同时 await DYNAMIC 类型的异步标记。
需要注意的问题
同步 get 的语义
container.get() 对于异步 toDynamicValue 返回的是什么?两种选择:
- 方案 A:返回 Promise 对象本身(与当前行为一致,向后兼容)
- 方案 B:返回 undefined 或抛出错误,强制用户使用
getAsync
方案 A 更安全,不会破坏现有代码。
属性注入的时机问题
如果 A 服务通过 @Inject 注入了一个异步 toDynamicValue 的依赖,get() 同步返回时注入的属性值是 Promise 对象。即使后来 Promise resolve 了,属性值也不会自动更新(属性注入是一次性赋值)。
这意味着异步 toDynamicValue 的完整使用链路必须是:
toDynamicValue(async () => ...) + @PostConstruct(true) + getAsync()三者缺一不可。这需要在文档中明确说明。
向后兼容性
所有改动必须保证:
- 同步
toDynamicValue的行为完全不变 container.get()的同步语义不受影响- 现有的
@PostConstruct只等待 INSTANCE 的行为在不涉及 DYNAMIC 时不变