规划阶段
规划阶段是每次从 InversifyJS 容器请求服务时发生的关键步骤。在创建任何实例或解析值之前,InversifyJS 会构建一个全面的规划树,准确地规划出如何满足你的依赖请求。
什么是规划树?
规划树是一个分层数据结构,描述了服务的完整解析策略。它捕获:
- 将使用哪些绑定来解析每个服务
- 每个服务需要哪些依赖项(构造函数参数和属性)
- 服务必须被解析的顺序
- 关于每个绑定及其解析要求的元数据
可以将其视为容器在解析阶段遵循的蓝图,用于构建和连接你的依赖关系图。
规划如何工作
当你调用 container.get() 或 container.getAll() 时,InversifyJS 执行以下步骤:
1. 缓存检查
首先,容器检查其缓存中是否已存在针对请求的服务标识符和约束(名称、标签等)的规划。如果找到匹配的规划,它将立即被重用,从而避免重复的规划工作。
2. 构建服务节点
如果不存在缓存的规划,InversifyJS 将为请求的服务构建一个服务节点:
- 约束构建:容器根据你的请求参数(服务标识符、名称、标签、可选/多个标志)构建约束列表
- 绑定过滤:使用每个绑定的
isSatisfiedBy()方法,根据这些约束过滤服务标识符的所有绑定 - 自动绑定:如果未找到绑定且启用了自动绑定,则会自动为该服务创建一个新的实例绑定
3. 处理每个绑定
对于每个匹配的绑定,InversifyJS 根据绑定类型创建一个绑定节点:
- 实例绑定:创建一个
InstanceBindingNode,其中包括类元数据,并递归规划所有构造函数参数和属性注入 - 解析值绑定:创建一个
ResolvedValueBindingNode,递归规划工厂函数所需的所有参数 - 服务重定向绑定:创建一个
PlanServiceRedirectionBindingNode,重定向到另一个服务标识符 - 叶绑定(常量、动态值、工厂、提供者):创建一个没有子依赖项的
LeafBindingNode
4. 递归依赖规划
对于具有依赖项的绑定(实例和解析值绑定),InversifyJS 递归地为每个依赖项构建规划节点。这将创建一个树结构,其中:
- 每个服务节点代表一个要解析的服务
- 每个绑定节点代表该服务将如何被解析
- 子节点代表该解析策略所需的依赖项
5. 惰性求值
为了优化性能,规划树的某些部分使用惰性求值。子依赖节点被包装在 LazyPlanServiceNode 实例中,这些实例将推迟其完整规划,直到在解析期间实际需要为止。这避免了在具有可选依赖项或条件逻辑的场景中规划未使用的分支。
6. 验证
构建树后,InversifyJS 会对其进行验证:
- 对于单个注入(
container.get()),确保只找到一个绑定(对于可选注入,则为零个) - 通过在规划期间跟踪服务分支并抛出描述性错误来检测循环依赖
- 验证是否满足所有必需的约束
7. 缓存
完成的规划树存储在容器的规划缓存中,由服务标识符和约束选项索引。对具有相同约束的同一服务的后续请求将重用此缓存的规划,从而使重复解析快得多。
上下文无关与上下文相关规划
规划树跟踪它们是上下文无关还是上下文相关:
- 上下文无关规划:不依赖于祖先服务(不使用基于祖先的约束)。这些可以在不同的解析上下文中安全地缓存和重用。
- 上下文相关规划:使用基于祖先的约束(例如,按父服务过滤)。这些仍然被缓存,但被标记为上下文相关。
规划树结构
完整的规划结果包含:
interface PlanResult {
tree: {
root: PlanServiceNode
}
}
每个 PlanServiceNode 包括:
serviceIdentifier:正在解析的服务bindings:描述如何解析它的一个或多个绑定节点isContextFree:此节点是否依赖于解析上下文
每个绑定节点包含:
binding:实际的绑定实例- 特定于类型的元数据(类元数据、参数、属性注入等)
- 依赖项的子服务节点
可视化规划树
使用下面的交互式代码编辑器尝试不同的绑定配置并查看生成的规划树。编辑器允许你编写 InversifyJS 代码并可视化生成的规划结构,帮助你了解容器如何解析你的依赖项。
性能考虑
规划阶段旨在高效:
- 缓存:规划被缓存并重用于相同的请求
- 惰性求值:不必要的子分支不会被完全求值
- 不可变数据结构:使用不可变链表进行高效的约束列表管理
- 早期验证:在规划期间而不是解析期间捕获配置错误
大部分计算成本发生在对服务的第一次请求期间。后续请求受益于缓存的规划,使其速度显著加快。