Skip to content

toDynamicValue 异步支持分析

背景

本库的 @PostConstruct 异步等待机制目前只支持 BINDING.INSTANCE 类型的服务。具体来说,_postConstruct 方法中(src/binding.ts 第 181 行附近)的过滤逻辑:

ts
const bindings = propertyBindings.filter(
  item => BINDING.INSTANCE === item?.type
);

这意味着当一个服务通过 @PostConstruct(true) 声明需要等待前置依赖初始化完成时,只会等待那些通过 to() / toSelf() 绑定的类实例服务,而不会等待 toConstantValuetoDynamicValue 绑定的服务。

当前现状分析

toConstantValue —— 无需支持异步

toConstantValue(value) 接收的是一个已经确定的值,在绑定时就已经传入,不涉及任何实例化过程,也没有生命周期钩子。常量天然是同步的,不存在异步初始化的需求。

用户当然可以写 toConstantValue(somePromise),但此时注入的是 Promise 对象本身,而不是 Promise 的结果,这是用户的主动选择,不属于框架需要处理的异步初始化问题。

结论:toConstantValue 可以忽略,无需支持异步。

toDynamicValue —— 存在异步需求

toDynamicValue 的类型签名当前是同步的:

ts
export type DynamicValue<T> = (ctx: Context) => T;

_resolveDynamicValue 也是同步执行:

ts
_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.INSTANCEpostConstructResult,还需要收集 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 时不变

参考资料