首页
/ Angular 依赖注入(DI)高级主题解析:Key、Injector 分层结构与 Host/Visibility 可见性边界

Angular 依赖注入(DI)高级主题解析:Key、Injector 分层结构与 Host/Visibility 可见性边界

2026-09-08 22:46:32作者:温艾琴Wonderful

本文以仓库内的 packages/docs/di/di_advanced.md 为骨架,结合 packages/docs/di/di.md 以及当前仓库中 packages/core 的 DI 实现源码,系统讲解 Angular 依赖注入中三类常被忽略的高级机制:Key(内部键)ProtoInjector 与 Injector 的职责分离,以及 Host 宿主边界与 Provider 可见性(Visibility)。读完本文,你将理解依赖查找"为什么向上走、在哪里停下、谁能看见什么",并能看懂 providersviewProviders 在视图树上形成的双重视图,从而在真实组件层级中精确设计服务的作用域。

这份文档定位特殊:它由 Angular 核心团队留存,深入描述 DI 模块的内部工作原理,且明确声明"不掌握这些内容也能正常使用 DI"。因此它更适合把 Angular 当作研究对象、或需要在性能敏感场景定制注入逻辑的读者。值得注意的是,文档中的示例大量使用早期架构的底层 API(bindInjector.resolveAndCreateKey.getProtoInjectorBindingWithVisibility 等),这些类已不再存在于当前源码树中(在 packages 下仅文档目录还能搜到它们),但其中的心智模型与可见性规则被完整保留在了现代 Ivy 的 NodeInjector 实现中。下文会先忠实还原原文档的推导过程,再给出与当前源码的对照。

文档背景:它讲述的是哪一代 DI

先厘清概念所处的时代,避免读者在 API 层面产生混淆:

  • 本文所述核心抽象(InjectorBindingDependency)是文档的姊妹篇 packages/docs/di/di.md 的主题。简单回顾:injector 由一组 binding 创建、负责解析依赖并创建对象;bindingtoken(字符串或类)映射到工厂函数与依赖列表;dependency 指向某个 token 并携带注入时的附加约束信息。
  • bind(Engine).toFactory(...)KeyProtoInjectorBindingWithVisibilityVisibility.Public/Private/PublicAndPrivate 这批 API 属于早期(Angular 2 起步阶段)的内部 DI 架构,后来经历了 Provider 对象标准化与 Ivy 重写。如今这些名称仅作为历史文档存留,但它们抽象出的问题(token 归一化、静态元数据与实例分离、解析边界与可见性)在每一代实现中都被继承。

理解"问题本身没变、只是实现换了名字",是阅读下文所有代码示例的正确姿势。

Key:token 到内部键的映射

大多数场景下开发者不需要直接接触 Key——这也是文档把它列为第一个高级主题的原因:你天天在用,只是没意识到它的存在。

普通写法中,token(这里是 Engine 类)会被自动映射成内部键:

var inj = Injector.resolveAndCreate([
  bind(Engine).toFactory(() => new TurboEngine())  //the passed in token Engine gets mapped to a key
]);
var engine = inj.get(Engine); //the passed in token Engine gets mapped to a key

显式使用 Key 的写法如下——注意此时不再发生映射:

var ENGINE_KEY = Key.get(Engine);

var inj = Injector.resolveAndCreate([
  bind(ENGINE_KEY).toFactory(() => new TurboEngine()) // no mapping
]);
var engine = inj.get(ENGINE_KEY);  // no mapping

核心机制就一句话:

每个 Key 都拥有一个 id,DI 用这个 id 来存储 provider 与实例。因此 Injector 在内部总是借助 Key 工作,纯粹是为了性能——拿整数 id 做哈希表键,比拿任意 token(尤其运行时对象)做键快得多。

把这段历史设计与当前代码对照,会发现"token 需要归一化为可高效检索的标识"这一诉求在现代实现中以另一种形态延续:Ivy 在实例化注入器时给每个指令 token 打上唯一的 NG_ELEMENT_ID 编号(见下文 "bloom filter" 部分,源码位于 packages/core/src/render3/di.ts),本质上仍是"给 token 分配稳定 id、用 id 驱动查找"。当初是 Key.id,今天是 NG_ELEMENT_ID,思路一脉相承。

ProtoInjector 与 Injector:静态元数据与运行实例的分离

问题:应用中常常需要创建大量"结构相同"的注入器。以 Angular 为例,每一种组件元素类型都会以完全相同的方式配置自己的注入器。如果对每个实例都重新解析一遍 provider,会非常浪费:

function createComponentInjector(parent, providers: Binding[]) {
  return parent.resolveAndCreateChild(providers);
}

上面的写法意味着:每创建一个组件实例,就要重新 resolve 并存储一次 providers。

解法:把"关于 injectable 的元信息(providers 及其依赖关系)"与"injectable 本身"分开存储:

  • ProtoInjector(原型注入器)持有静态元信息——providers 与依赖关系只解析一次,按组件类型复用;
  • Injector 持有运行期实例——按组件实例创建,内部引用同一个 proto。
var proto = new ProtoInjector(providers); // done once
function createComponentInjector(parent, proto) {
  return new Injector(proto, parent);
}

便捷入口 Injector.resolveAndCreate 做的事其实是把两者一并创建出来:先 resolve 出 proto,再基于 proto 建出 injector。同理,resolveAndCreateChild 之于父子注入器也是同样两步。

这与 packages/docs/di/di.md 中"先用 Injector.resolve([...]) 得到已解析 provider、再用 fromResolvedProviders 快速建注入器"的思路完全一致——预先 resolve、按需复用,是性能敏感区域的推荐做法。

对照现代 Ivy:编译期的 TView.data / 指令定义(ɵdir/ɵcmp 中的 providers、viewProviders 数组)承担了"按类型存储元信息"的角色,运行期的 LView 数组则按实例存放已创建的 injectable 与注入器结构(索引与 TView.data 一一对应)。getOrCreateNodeInjectorForNode 只在"元素上确有指令/provider"且尚未创建时才初始化节点注入器——这正是下文"每个元素都有一个 Injector"心智模型的工程化落点。

Host 宿主边界与 Provider 可见性

什么是 Host 边界

注入器可以有父注入器,而父关系可以被显式标记为 host(宿主)

var child = new Injector(proto, parent, true /* host */);

Host 标记的作用是约束依赖解析的搜索范围:解析遇到 host 便停止向上。例如下面的构造器声明了 @Host(),那么 DI 在沿着注入器链找到 host 之后,就不会再继续往 host 之上寻找 Engine

class Car {
  constructor(@Host() e: Engine) {}
}

Public / Private / PublicAndPrivate 三档可见性

设想这样的注入器拓扑——ParentInjector 同时拥有两类子注入器,其中 Child2 把自己与父节点的关系标记为 host:

    ParentInjector
       /   \
      /     \ host
   Child1    Child2

此时 ParentInjector 可能需要向"普通子节点"与"host 子节点"暴露两套不同的 providers

  • 对"普通子节点"可见的 provider 称为 public(公共)
  • 对"host 子节点"可见的 provider 称为 private(私有)

这是 Angular 内部真实使用的高级场景:组件可以分别向"它的子级"和"它的视图"提供不同集合的服务。理解了这一点,providersviewProviders 的存在理由就呼之欲出了。

下面用三组实验把语义讲透。先看默认情况——Engine 标记为 public:

class Car {
  constructor(@Host() e: Engine) {}
}

var parentProto = new ProtoInjector([
  new BindingWithVisibility(Engine, Visibility.Public),
  new BindingWithVisibility(Car, Visibility.Public)
]);
var parent = new Injector(parentProto);

var hostChildProto = new ProtoInjector([new BindingWithVisibility(Car, Visibility.Public)]);
var hostChild = new Injector(hostChildProto, parent, true);

var regularChildProto = new ProtoInjector([new BindingWithVisibility(Car, Visibility.Public)]);
var regularChild = new Injector(regularChildProto, parent, false);

hostChild.get(Car); // will throw because public dependencies declared at the host cannot be seen by child injectors
parent.get(Car); // this works
regularChild.get(Car); // this works

结论 AVisibility.Public 的 provider 只对"普通子节点"可见;当一个 host 子节点沿链向上搜索时,宿主上标记为 public 的 provider 对它不可见,于是抛错。注意 Car 本身仍在子节点自己的 proto 中声明,所以 hostChild.get(Car) 的失败并非找不到 Car,而是实例化 Car 时无法解析 @Host() Engine

再把 Engine 标记为 private:

class Car {
  constructor(@Host() e: Engine) {}
}

var parentProto = new ProtoInjector([
  new BindingWithVisibility(Engine, Visibility.Private),
  new BindingWithVisibility(Car, Visibility.Public)
]);
var parent = new Injector(parentProto);

var hostChildProto = new ProtoInjector([new BindingWithVisibility(Car, Visibility.Public)]);
var hostChild = new Injector(hostChildProto, parent, true);

var regularChildProto = new ProtoInjector([new BindingWithVisibility(Car, Visibility.Public)]);
var regularChild = new Injector(regularChildProto, parent, false);

hostChild.get(Car); // this works
parent.get(Car); // this throws
regularChild.get(Car); // this throws

结论 BVisibility.Private 的 provider 只对"host 子节点"可见。于是宿主自己取不到、普通子节点也取不到 Engine(因为二者的解析视角都看不到它),唯有 host 子节点可以。这正是组件视图(由宿主边界隔开的子注入器)读取 viewProviders 的底层模型。

最后,把 Engine 同时标记为 public 与 private:

class Car {
  constructor(@Host() e: Engine) {}
}

var parentProto = new ProtoInjector([
  new BindingWithVisibility(Engine, Visibility.PublicAndPrivate),
  new BindingWithVisibility(Car, Visibility.Public)
]);
var parent = new Injector(parentProto);

var hostChildProto = new ProtoInjector([new BindingWithVisibility(Car, Visibility.Public)]);
var hostChild = new Injector(hostChildProto, parent, true);

var regularChildProto = new ProtoInjector([new BindingWithVisibility(Car, Visibility.Public)]);
var regularChild = new Injector(regularChildProto, parent, false);

hostChild.get(Car); // this works
parent.get(Car); // this works
regularChild.get(Car); // this works

结论 CVisibility.PublicAndPrivate 对两类子节点都可见,最典型的用途就是组件自身——组件既要能被自己的普通子级注入,也要能被自己的视图注入。

三种可见性速查

可见性 普通子节点(Child1) host 子节点(Child2 / 视图) 宿主自身
Public
Private
PublicAndPrivate

Host 概念在现代源码中的落点是注入标志位:InternalInjectFlags 用二进制位定义了解析约束,其中 Host = 0b0001Self = 0b0010SkipSelf = 0b0100(见 packages/core/src/di/interface/injector.ts),而 @Host 装饰器在 packages/core/src/di/metadata.ts 中通过 attachInjectFlag(makeParamDecorator('Host'), InternalInjectFlags.Host) 实现——它本质就是给参数注入信息打上一个"遇到宿主即止"的位标记。普通约束 @Self(止于当前注入器)、@SkipSelf(从父注入器开始向上)与 @Host(止于最近的宿主边界)三者在 packages/docs/di/di.md 中有更完整的解析算法说明。

Angular 如何应用 DI:每个元素一个 Injector

心智模型

理解 Angular 中 DI 的正确心智模型是:

每个 DOM 元素上都挂着一个 Injector。(实践中只有"含有指令的有趣元素"才会真正创建注入器,这纯粹是性能优化。)

于是 HTML 树直接对应出一棵注入器树。配置这棵树的两个核心属性是:

  • providers:作用于当前元素及其子元素
  • viewProviders:作用于组件自己的视图

可见性规则的表述是:

任何指令都能通过 providers 声明可注入项,但只有组件能声明 viewProviders

viewProviderspackages/core/src/metadata/directives.tsComponent 接口中定义("Defines the set of injectable objects that are visible to its view DOM children"),且只存在于 Component 而非 Directive 上,与上述规则一致。

一个复杂例子:注入器树如何生成

考虑如下模板——MyComponentMyDirective 创建在同一个元素上,元素内部还嵌了两个子元素:

<my-component my-directive>
  <needs-service></needs-service>
</my-component>

对应的组件与指令声明:

@Component({
  selector: 'my-component',
  providers: [
    bind('componentService').toValue('Host_MyComponentService')
  ],
  viewProviders: [
    bind('viewService').toValue('View_MyComponentService')
  ],
  template: `<needs-view-service></needs-view-service>`,
  directives: [NeedsViewService]
})
class MyComponent {}

@Directive({
  selector: '[my-directive]',
  providers: [
    bind('directiveService').toValue('MyDirectiveService')
  ]
})
class MyDirective {
}

两个需要注入服务的子指令:

@Directive({
  selector: 'needs-view-service'
})
class NeedsViewService {
  constructor(@Host() @Inject('viewService') viewService) {}
}

@Directive({
  selector: 'needs-service'
})
class NeedsService {
  constructor(@Host() @Inject('componentService') service1,
              @Host() @Inject('directiveService') service2) {}
}

最终生成的注入器树如下:

      Injector1 [
        {binding: MyComponent,        visibility: Visibility.PublicAndPrivate},
        {binding: 'componentService', visibility: Visibility.PublicAndPrivate},
        {binding: 'viewService',      visibility: Visibility.Private},
        {binding: MyDirective         visibility: Visibility.Public},
        {binding: 'directiveService', visibility: Visibility.Public}
      ]
  /                                                       \
  |                                                        \ host
Injector2 [                                                 Injector3 [
  {binding: NeedsService, visibility: Visibility.Public}           {binding: NeedsViewService, visibility: Visibility.Public}
]                                                           ]

对照模板逐条验证,能得出三条清晰规则:

  1. Injector1(宿主元素)MyComponent 自身与它的 providerscomponentService)是 PublicAndPrivate——组件既要"看见自己视图里的注入需求",也要让"外层元素"能看到。viewProvidersviewService)是 Private——只对视图方向可见。MyDirective 及它的 directiveServicePublic——它只是普通指令,服务只对自己的子级(即 NeedsService 所在的外层子元素)可见。
  2. Injector2<needs-service>,普通子节点):能注入 componentService(父级 PublicAndPrivate)与 directiveService(父级 Public),且二者都加了 @Host()——查找恰好在 Injector1 这个宿主边界内完成。
  3. Injector3<needs-view-service>,组件视图,host 子节点):沿 host 边界向上能看见 viewService(Private 对视图可见)与 MyComponent 自身,但看不见 directiveService(Public 对 host 子节点不可见),因此 NeedsViewService 只注入 viewService 一个依赖。

一句话总结这幅图:

组件自身及其 providers,既能被子级看见也能被视图看见;viewProviders 只有视图能看见;其它指令的 providers 只有它们的子级能看见。

现代 Ivy 实现对照:可见性如何被编码

原文档的 BindingWithVisibilityProtoInjector 已从代码库消失,但三条可见性规则在现代 Ivy 的 NodeInjector 中依然成立,且工程实现更为精巧。相关逻辑集中在 packages/core/src/render3/di.ts

  • per-element、按需创建getOrCreateNodeInjectorForNode 只对真正需要的节点初始化注入器,并在 tView.datalView 中铺设 bloom filter 数组(BLOOM_SIZE = 256 槽位,8 个 32 位桶)。每个 token 被分配全局唯一 NG_ELEMENT_ID 后按位取模写入过滤器,查找时用 bloomHasToken 做 O(1) 预筛,把"绝大多数注入器根本没有这个 provider"的情况以极低成本排除。
  • includeViewProviders 开关:模块级变量 includeViewProviders 控制当前实例化能否访问 viewProviders。源码注释给出典型场景:组件构造函数里 inject 字符串 token 时应命中 viewProviders(值为 'viewProviders'),而组件自己通过 providers 提供的 MyService 内再注入同一个 token 时,则必须命中 providers(值为 'providers')——getNodeInjectable 在调用工厂前后通过 setIncludeViewProviders(factory.canSeeViewProviders) 保存与恢复该开关,保证"声明在 viewProviders 之外的服务无法偷窥 viewProviders"。这与文档中"viewService 是 Private"的规则完全同构。
  • @Host 的宿主角:解析时若带有 Host 标志,lookupTokenUsingNodeInjector 会记录 hostTElementNodeshouldSearchParent 遇到"第一个 host 节点"即终止向上;而 locateDirectiveOrProvider 中存在一个明确的 host 特例——当查找点恰是 host 元素时,endIndex 被限制在 injectablesStart + cptViewProvidersCount,即只能看到组件自身的 viewProviders 与组件类型本身,其余普通 provider 与兄弟指令一概不可见。这正是文档"public dependencies declared at the host cannot be seen by host child injectors"的现代实现。

对照原文档树中的 Injector1,现代编译产物会把 componentServiceviewServicedirectiveService 等按 "component viewProviders(私有段)→ component providers → directive providers"的顺序排布在节点 provider 区间,cptViewProvidersCount 记录私有段长度,canAccessViewProviders 决定从私有段还是公共段开始扫描——三段可见性被压缩成了"一段数组 + 两个偏移量",比 BindingWithVisibility 时代更为紧凑。

结语与自查清单

di_advanced.md 的价值不在于那几个已经被淘汰的类名,而在于它揭示了 DI 设计中反复出现的三条不变法则,可在排错时作为自查清单:

  1. token 会被归一化:注入器内部永远以"带 id 的键 / 带编号的 bloom 位"工作,inj.get(Engine) === inj.get(Key.get(Engine)) 只是同一件事的两种写法。
  2. 元信息与实例分离:provider 解析一次、按类型共享(ProtoInjector / TView),实例按需创建并缓存(Injector / LView)——这也是 Injector.resolveAndCreate 会"偷偷"先建 proto 的原因。
  3. 可见性决定谁能拿到服务providers 面向元素与子级、viewProviders 面向组件视图、@Host 划定查找的终点;当注入结果与直觉不符时,先画出元素树,标出哪条边是 host 边界,再套用 Public/Private 规则即可定位问题。

对普通应用开发者,只要遵守"把跨组件共享的服务放进 providers、把仅限本组件视图使用的服务放进 viewProviders"这一实践准则,便已足够;本文所述机制更适合在需要理解或调试复杂嵌套组件层级、封装可复用组件库时翻阅。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393