首页
/ Angular 分层依赖注入详解:注入器层级、解析规则与可见性控制

Angular 分层依赖注入详解:注入器层级、解析规则与可见性控制

2026-09-06 15:43:00作者:邵娇湘

本文基于 Angular 官方指南 Hierarchical injectors 编写,系统讲解 Angular 分层依赖注入(DI)体系的完整脉络:EnvironmentInjectorElementInjector 两大注入器层级、两阶段 token 解析规则、optional/self/skipSelf/host 四种解析修饰符,以及 providersviewProviders 在模板逻辑树中的可见性差异。读完本文,你可以结合仓库源码(InjectOptions 接口定义R3Injector 查找实现NodeInjector 解析入口)精确控制服务在组件树中的可见范围,理解 #VIEW 边界与内容投影下的注入行为。

基础概念(如注入器层级与 provider 作用域的入门介绍)可参考 defining dependency providers 指南

两大注入器层级

Angular 中存在两种注入器层级:

注入器层级 说明
EnvironmentInjector 层级 通过 @Service() 装饰器,或在 ApplicationConfigproviders 数组中配置
ElementInjector 层级 在每个 DOM 元素上隐式创建。ElementInjector 默认为空,除非你在 @Directive()@Component()providers 属性中为它配置了 provider

NgModule 应用:对于基于 NgModule 的应用,还可以通过 @NgModule()@Injectable() 注解,使用 ModuleInjector 层级来提供依赖。

EnvironmentInjector:两种配置方式

EnvironmentInjector 可以通过以下两种方式配置:

  • @Service() 装饰器
  • ApplicationConfigproviders 数组

推荐优先使用 @Service():与把服务写进 ApplicationConfig.providers 不同,@Service() 允许优化工具执行 tree-shaking(摇树优化),把应用未使用的服务从产物中剔除,从而减小打包体积。这对库(library)尤其有用——使用库的应用方可能根本不需要注入该库的服务。

import {Service} from '@angular/core';

@Service() // 将该服务提供到根 EnvironmentInjector
export class ItemService {
  name = 'telephone';
}

@Service()@Injectable() 装饰器都用于标识一个服务类。

ModuleInjector:NgModule 应用的层级

NgModule 应用中,ModuleInjector 可以通过以下方式配置:

  • @Service() 装饰器
  • @Injectable()providedIn 属性(指向 rootplatform
  • @NgModule()providers 数组

ModuleInjector@NgModule.providersNgModule.imports 共同决定——它是沿 NgModule.imports 递归可达的所有 providers 数组合并(flatten)的结果。当通过懒加载引入其他 @NgModule 时,会创建子级的 ModuleInjector 层级。

Platform 注入器与 NullInjector:层级树的顶端

root 之上还有两个注入器:一个额外的 EnvironmentInjector(平台注入器)和 NullInjector

考虑 main.ts 中的应用引导:

bootstrapApplication(App, appConfig);

bootstrapApplication() 会创建一个平台注入器的子注入器,由 ApplicationConfig 实例配置,这就是 root EnvironmentInjector

platformBrowserDynamic() 则创建由 PlatformModule 配置的注入器,其中存放平台相关依赖。这允许多个应用共享同一份平台配置——例如浏览器只有一个地址栏,无论运行多少应用。你也可以通过 platformBrowser() 函数传入 extraProviders 在平台层配置额外的 provider。

层级树的顶端是 NullInjector。如果你在向上查找时一直查到了 NullInjector,就会抛错——除非使用了 @Optional()。因为所有查找最终都会终结在 NullInjector:它返回错误,或在 @Optional() 情形下返回 null

这一行为在源码中直接可见,NullInjector 的实现只有寥寥数行:notFoundValueTHROW_IF_NOT_FOUND 时抛出 "No provider found" 运行时错误,否则原样返回 notFoundValue

export class NullInjector implements Injector {
  get(token: any, notFoundValue: any = THROW_IF_NOT_FOUND): any {
    if (notFoundValue === THROW_IF_NOT_FOUND) {
      const message = ngDevMode ? `No provider found for \`${stringify(token)}\`.` : '';
      const error = createRuntimeError(message, RuntimeErrorCode.PROVIDER_NOT_FOUND);
      error.name = 'ɵNotFound';
      throw error;
    }
    return notFoundValue;
  }
}

以下状态图展示了 root 注入器与其父注入器的关系:

stateDiagram-v2
    elementInjector: EnvironmentInjector<br>(由 Angular 配置)<br>存放 DomSanitizer 等 => providedIn 'platform'
    rootInjector: root EnvironmentInjector<br>(由 AppConfig 配置)<br>存放应用所需 => bootstrapApplication(..., AppConfig)
    nullInjector: NullInjector<br>除非使用 @Optional()<br>否则永远抛错

    direction BT
    rootInjector --> elementInjector
    elementInjector --> nullInjector

root 是一个特殊别名,其他 EnvironmentInjector 层级没有别名。你可以在动态加载组件创建时(例如 Router)随时创建新的 EnvironmentInjector 子层级。无论 root 是通过传给 bootstrapApplication()ApplicationConfig 配置的,还是各服务自行用 root 注册了全部 provider,所有请求最终都会向上转发到根注入器。

@Injectable()ApplicationConfig 的优先级:如果你在 bootstrapApplicationApplicationConfig 中配置了应用级 provider,它会覆盖 @Injectable() 元数据中为 root 配置的 provider。一个典型场景是为路由配置非默认的 LocationStrategy

providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}];

对于 NgModule 应用,则在 AppModuleproviders 中配置应用级 provider。

ElementInjector:每个组件实例一份

Angular 会为每个 DOM 元素隐式创建 ElementInjector 层级。在 @Component() 中通过 providersviewProviders 提供服务的,就是 ElementInjector

@Component({
  /* … */
  providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent

在组件中提供的服务经由该组件实例的 ElementInjector 可见;依据解析规则,它也可能对子组件/指令可见。组件实例被销毁时,其服务实例也随之销毁——这是组件级 provider 的生命周期语义。

由于组件是特殊类型的指令,@Directive() 一样有 providers 属性:指令和组件都可以配置 provider,且同一元素上的组件与指令共享同一个注入器。

解析规则:两阶段查找

当为一个组件/指令解析 token 时,Angular 分两个阶段进行:

  1. ElementInjector 层级:先沿着元素注入器树的祖先向上找;
  2. EnvironmentInjector 层级:再从请求起源的元素出发,在环境注入器层级中查找。

具体过程:组件声明了依赖后,Angular 先尝试用其自身的 ElementInjector 满足;找不到就传给父组件的 ElementInjector,请求持续向上转发,直到某个注入器能处理,或祖先 ElementInjector 耗尽。若所有 ElementInjector 都找不到,Angular 回到发起请求的元素,在 EnvironmentInjector 层级中查找;仍找不到则抛错。

对于 NgModule 应用,如果在 ElementInjector 层级中找不到 provider,Angular 会继续搜索 ModuleInjector 层级。

同名 token 就近原则:如果同一个 DI token 在多个层级注册了 provider,Angular 使用它在查找路径上第一个遇到的那个。例如 provider 就注册在需要该服务的组件本地,Angular 就不会再去寻找其他 provider。

源码印证:ElementInjector 的解析入口是 getOrCreateInjectable,其查找顺序与文档描述完全一致——先查嵌入式视图注入器(embedded view injector),再沿节点注入器树 lookupTokenUsingNodeInjector 向上爬,最后 lookupTokenUsingModuleInjector 兜底到模块/环境注入器。而 EnvironmentInjector 侧的 R3Injector.get() 则实现了修饰符语义:

// packages/core/src/di/r3_injector.ts(简化摘录)
if (!(flags & InternalInjectFlags.SkipSelf)) {
  // SkipSelf 未设置:先查本注入器的 records / injectableDef
  ...
}
// Self 标志决定"下一个注入器":self 为 true 时终结于 NullInjector,否则走 parent
const nextInjector = !(flags & InternalInjectFlags.Self) ? this.parent : getNullInjector();
// Optional 标志:未找到时 notFoundValue 被改写为 null
notFoundValue =
  flags & InternalInjectFlags.Optional && notFoundValue === THROW_IF_NOT_FOUND
    ? null
    : notFoundValue;
return nextInjector.get(token, notFoundValue);

这四行即"skipSelf 决定起点、self 决定终点、optional 决定失败返回值"的实现核心。

解析修饰符(Resolution Modifiers)

Angular 的解析行为可用 optionalselfskipSelfhost 修改。它们都从 @angular/core 导入,写在 inject() 的配置参数里。其类型定义见 InjectOptions 接口

export interface InjectOptions {
  /** 使用可选注入,未找到时返回 `null`。 */
  optional?: boolean;
  /** 从当前注入器的父注入器开始注入。 */
  skipSelf?: boolean;
  /** 只查询当前注入器,不向上级联。 */
  self?: boolean;
  /** 在宿主组件注入器处停止注入。仅对元素注入器有意义,对环境注入器无效。 */
  host?: boolean;
}

修饰符分类

解析修饰符分为三类:

  • 没找到时怎么办optional
  • 从哪里开始找skipSelf
  • 到哪里停止找hostself

默认情况下,Angular 始终从当前注入器开始、一路向上搜索。修饰符的作用就是改变起点(self)或终点。

可任意组合,但以下两组互斥

  • hostself
  • skipSelfself

源码中修饰符以位标志(InternalInjectFlags,见 interface/injector.ts)编码:Host = 0b0001Self = 0b0010SkipSelf = 0b0100Optional = 0b1000,这也解释了为何 selfskipSelfselfhost 在语义上互斥。

optional

optional 让 Angular 把注入的服务视为可选:若运行时解析不到,返回 null 而不是抛错。下例中 OptionalService 未在 ApplicationConfig@NgModule() 或任何组件中提供,整个应用中都不可用:

export class Optional {
  public optional? = inject(OptionalService, {optional: true});
}

self

self 让 Angular 查看当前组件/指令的 ElementInjector。典型用途是"仅当宿主元素上恰好提供该服务时才注入它",此时应把 selfoptional 组合以避免报错:

@Component({
  selector: 'app-self-no-data',
  templateUrl: './self-no-data.html',
  styleUrls: ['./self-no-data.css'],
})
export class SelfNoData {
  public leaf = inject(LeafService, {optional: true, self: true});
}

在这个例子中即使存在父级 provider,注入也会返回 null,因为 self 要求查找在当前宿主元素处终止。

相反的例子:组件自身提供了 FlowerService,注入器查到即止,返回郁金香:

@Component({
  selector: 'app-self',
  templateUrl: './self.html',
  styleUrls: ['./self.css'],
  providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
})
export class Self {
  public flower = inject(FlowerService, {self: true});
}

skipSelf

skipSelfself 的反面:查找从父 ElementInjector 开始,跳过当前注入器。假设父组件使用的服务值是蕨类叶片 🌿:

export class LeafService {
  emoji = '🌿';
}

而子组件的 providers 里是枫叶 🍁,但你希望拿到父级的值——这正是 skipSelf 的用途:

@Component({
  selector: 'app-skipself',
  templateUrl: './skipself.html',
  styleUrls: ['./skipself.css'],
  // Angular 会忽略这个 LeafService 实例
  providers: [{provide: LeafService, useValue: {emoji: '🍁'}}],
})
export class Skipself {
  // 将 skipSelf 作为 inject 选项
  public leaf = inject(LeafService, {skipSelf: true});
}

此时 emoji 的取值是 🌿 而非 🍁。

skipSelf 与 optional 组合

skipSelfoptional 一起使用,可在值为 null 时避免报错。Person 在属性初始化期间注入自身 token,skipSelf 跳过当前注入器,optional 保证查不到时为 null

class Person {
  parent = inject(Person, {optional: true, skipSelf: true});
}

host

host 把某个组件指定为注入器树查找的最后站点:即使树上更高层级存在服务实例,Angular 也不会继续向上。用法如下:

@Component({
  selector: 'app-host',
  templateUrl: './host.html',
  styleUrls: ['./host.css'],
  // 提供服务
  providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
})
export class Host {
  // 注入服务时使用 host
  flower = inject(FlowerService, {host: true, optional: true});
}

由于 Host 带了 host 选项,无论父级 flower.emoji 是什么,Host 都用 🌷。

从源码注释看,hostself 的精确区别是:Self 标志"不向请求注入的节点的祖先上溯",而 Host 标志"检索任意注入器,直到抵达当前组件的宿主元素"(见 InternalInjectFlags 注释)。换言之 self 完全禁止上溯,host 允许上溯但以宿主元素为界。在节点注入器查找中,hostTElementNode 正是依据 Host 标志取 lView[DECLARATION_COMPONENT_VIEW][T_HOST] 来确定搜索上界的(见 render3/di.ts)。

构造函数注入中的修饰符

构造函数注入的行为可以用装饰器 @Optional()@Self()@SkipSelf()@Host() 做同样的修改,从 @angular/core 导入并用在构造函数参数上:

export class SelfNoData {
  constructor(@Self() @Optional() public leaf?: LeafService) {}
}

这些装饰器在 metadata.ts 中通过 attachInjectFlag 绑定到对应的 InternalInjectFlags 位,最终与 inject() 选项走同一条标志解析路径。

模板的逻辑结构:<#VIEW> 边界

在组件类中提供服务时,服务的可见范围取决于你在哪里、以何种方式提供它们。理解 Angular 模板的底层逻辑结构,是配置服务、控制可见性的基础。

组件在模板中是这样使用的:

<app-root> <app-child />; </app-root>

组件及其模板通常写在不同文件里。为了理解注入系统,把它们看成一棵**合并的逻辑树(logical tree)**很有帮助——注意"逻辑树"与渲染树(即应用 DOM 树)不同。为标示组件模板的位置,这里使用 <#VIEW> 伪元素。它并不真实存在于渲染树中,纯粹是心智模型的标记:

<app-root>
  <#VIEW>
    <app-child>
     <#VIEW>
       …content goes here…
     </#VIEW>
    </app-child>
  </#VIEW>
</app-root>

理解 <#VIEW> 的界定,对配置组件类中的服务尤其关键。

示例:在 @Component() 中提供服务

@Component()(或 @Directive())提供服务的方式决定了服务的可见性。组件类提供服务的两种数组:

数组 说明
providers 数组 @Component({ providers: [SomeService] })
viewProviders 数组 @Component({ viewProviders: [SomeService] })

下文的逻辑树代表应用的 HTML 结构,例如展示 <child-component><parent-component> 的直接子节点。逻辑树中出现特殊属性 @Provide@Inject@ApplicationConfig——它们不是真实的 HTML 属性,只是用来演示底层发生了什么:

属性 含义
@Inject(Token)=>Value Token 在此处被注入,其值为 Value
@Provide(Token=Value) 表示 Token 在此处以 Value 提供
@ApplicationConfig 表示此处应使用回退的 EnvironmentInjector

示例应用结构

示例应用在 root 提供 FlowerServiceemoji 值为红色朱槿 🌺:

@Service()
export class FlowerService {
  emoji = '🌺';
}

应用只有 AppChild。最基础的渲染视图是嵌套的 HTML 元素:

<app-root>
  <!-- App selector -->
  <app-child> <!-- Child selector --> </app-child>
</app-root>

但 Angular 解析注入请求时,背后使用的是逻辑视图表示:

<app-root> <!-- App selector -->
  <#VIEW>
    <app-child> <!-- Child selector -->
      <#VIEW>
      </#VIEW>
    </app-child>
  </#VIEW>
</app-root>

<#VIEW> 代表一个模板实例,注意每个组件都有自己的 <#VIEW>

现在让 <app-root> 注入 FlowerService

export class App {
  flower = inject(FlowerService);
}

<app-root> 模板加绑定以可视化结果:

<p>Emoji from FlowerService: {{flower.emoji}}</p>

视图输出:

Emoji from FlowerService: 🌺

逻辑树表示为:

<app-root @ApplicationConfig
        @Inject(FlowerService) flower=>"🌺">
  <#VIEW>
    <p>Emoji from FlowerService: {{flower.emoji}} (🌺)</p>
    <app-child>
      <#VIEW>
      </#VIEW>
    </app-child>
  </#VIEW>
</app-root>

<app-root> 请求 FlowerService 时,注入器的职责是解析该 token,分两个阶段:

  1. 注入器确定逻辑树中的起点终点,从起点出发在每个视图层寻找 token;找到即返回。
  2. 若未找到,注入器寻找最近的父级 EnvironmentInjector 委托请求。

本例中的约束是:

  1. 从属于 <app-root><#VIEW> 开始,到 <app-root> 结束。
    • 通常查找起点在注入点。但此处 <app-root> 是组件,@Component 比较特殊——查找会包含它自己的 viewProviders,所以起点是 <app-root><#VIEW>。若同一位置匹配的是指令则不是这种情况。
    • 终点恰好是组件本身,因为它是应用中最顶层的组件。
  2. ApplicationConfig 提供的 EnvironmentInjector 充当回退注入器,当 token 在 ElementInjector 层级中找不到时使用。

使用 providers 数组

Child 类中为 FlowerService 添加一个 provider,以演示更复杂的解析规则:

@Component({
  selector: 'app-child',
  templateUrl: './child.html',
  styleUrls: ['./child.css'],
  // 用 providers 数组提供服务
  providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}],
})
export class Child {
  // 注入服务
  flower = inject(FlowerService);
}

由于 FlowerService 已在 @Component() 装饰器中提供,<app-child> 请求该服务时,注入器只需查到 <app-child>ElementInjector 为止,不必再向注入器树更深处查找。

Child 模板添加绑定:

<p>Emoji from FlowerService: {{flower.emoji}}</p>

为了让视图同时显示向日葵,把 <app-child> 加到 App 模板底部:

Child Component
Emoji from FlowerService: 🌻

逻辑树表示为:

<app-root @ApplicationConfig
          @Inject(FlowerService) flower=>"🌺">
  <#VIEW>
  <p>Emoji from FlowerService: {{flower.emoji}} (🌺)</p>
  <app-child @Provide(FlowerService="🌻" )
             @Inject(FlowerService)=>"🌻"> <!-- search ends here -->
    <#VIEW> <!-- search starts here -->
    <h2>Child Component</h2>
    <p>Emoji from FlowerService: {{flower.emoji}} (🌻)</p>
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

<app-child> 请求 FlowerService 时,注入器从属于 <app-child><#VIEW> 开始(因为从 @Component() 注入,<#VIEW> 被包含进来),到 <app-child> 结束。FlowerService<app-child>providers 数组中以向日葵 🌻 被解析,注入器不必再往上查,它一找到就停下,永远看不到红色朱槿 🌺。

使用 viewProviders 数组

viewProviders 是另一种在 @Component() 中提供服务的方式,它使服务在 <#VIEW> 内部可见。

为演示 viewProviders,构建一个 emoji 值为鲸鱼 🐳 的 AnimalService

import {Service} from '@angular/core';

@Service()
export class AnimalService {
  emoji = '🐳';
}

按与 FlowerService 相同的模式,在 App 类中注入 AnimalService

export class App {
  public flower = inject(FlowerService);
  public animal = inject(AnimalService);
}

FlowerService 相关代码可以保留,便于与 AnimalService 对比。

<app-child> 类中也添加 viewProviders 数组并注入 AnimalService,但给 emoji 不同的值——这里是狗 🐶:

@Component({
  selector: 'app-child',
  templateUrl: './child.html',
  styleUrls: ['./child.css'],
  // 提供服务
  providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}],
  viewProviders: [{provide: AnimalService, useValue: {emoji: '🐶'}}],
})
export class Child {
  // 注入服务
  flower = inject(FlowerService);
  animal = inject(AnimalService);
}

ChildApp 模板中都添加绑定:

<p>Emoji from AnimalService: {{animal.emoji}}</p>

浏览器中现在应该看到两个值:

App
Emoji from AnimalService: 🐳

Child Component
Emoji from AnimalService: 🐶

viewProviders 示例的逻辑树:

<app-root @ApplicationConfig
          @Inject(AnimalService) animal=>"🐳">
  <#VIEW>
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    @Inject(AnimalService=>"🐶")>

    <!-- ^^using viewProviders means AnimalService is available in <#VIEW>-->
    <p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

FlowerService 示例一样,AnimalService<app-child>@Component() 装饰器中提供,注入器首先查看组件的 ElementInjector,找到狗 🐶 的值,既不需要继续搜索 ElementInjector 树,也不需要搜索 ModuleInjector

providers 与 viewProviders 的差异:内容投影边界

viewProvidersproviders 概念相似,但有一个显著差异:viewProviders 中的 provider 只在组件自身视图内可见——通过 <ng-content> 投影到组件中的内容看不到它们

为看清差异,再添加一个子于 Child 的组件 Inspector。在 inspector.ts 中于属性初始化期间注入两个服务:

export class Inspector {
  flower = inject(FlowerService);
  animal = inject(AnimalService);
}

inspector.html 添加同样的标记:

<p>Emoji from FlowerService: {{flower.emoji}}</p>
<p>Emoji from AnimalService: {{animal.emoji}}</p>

记得把 Inspector 加入 Childimports 数组:

@Component({
  ...
  imports: [Inspector]
})

child.html 中添加:

...

<div class="container">
  <h3>Content projection</h3>
  <ng-content />
</div>
<h3>Inside the view</h3>

<app-inspector />

<ng-content> 允许内容投影,<app-inspector> 位于 Child 模板中,使 Inspector 成为 Child 的子组件。

再在 app.html 中利用内容投影:

<app-child>
  <app-inspector />
</app-child>

浏览器现在渲染(省略前述示例):

...
Content projection

Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐳

Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐶

这四个绑定演示了 providersviewProviders 的差别。狗 🐶 声明在 Child<#VIEW> 内,投影内容无法看到它,投影出来的 <app-inspector> 看到的是鲸鱼 🐳。

为什么投影的 <app-inspector> 还能看到 App viewProviders 里的 🐳? 因为 Angular DI 追踪的是组件在哪里被声明,而不是它最终渲染在哪里。<app-inspector> 声明在 App 的模板里——位于 App<#VIEW> 内——所以 AppviewProviders 对它是可达的。把它投影进 Child 只是切断了访问 Child viewProviders(🐶)的通路,但沿树上行仍然能拿到 App 的 provider(🐳)。

而下一个输出里,InspectorChild 的真正子组件,位于 <#VIEW> 之内,请求 AnimalService 时看到的就是狗 🐶。

AnimalService 的逻辑树全貌:

<app-root @ApplicationConfig
          @Inject(AnimalService) animal=>"🐳">
  <#VIEW>
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    @Inject(AnimalService=>"🐶")>

    <!-- ^^using viewProviders means AnimalService is available in <#VIEW>-->
    <p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>

    <div class="container">
      <h3>Content projection</h3>
      <app-inspector @Inject(AnimalService) animal=>"🐳">
        <p>Emoji from AnimalService: {{animal.emoji}} (🐳)</p>
      </app-inspector>
    </div>

    <app-inspector>
      <#VIEW @Inject(AnimalService) animal=>"🐶">
      <p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
    </
    #VIEW>
    </app-inspector>
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>
  • 投影的 <app-inspector> 得到 🐳:因为 🐶 属于 Child 的视图,投影内容跨不过边界;而 🐳 可达,是因为 <app-inspector> 声明在 App 的模板中,可以一路上行到 AppviewProviders
  • 直接位于 Child 模板中的 <app-inspector>(非投影)得到 🐶——它在 <#VIEW> 之内,无需跨越任何边界。

已提供 token 的可见性:修饰符在逻辑树上的作用

可见性修饰符影响注入 token 查找在逻辑树中的起点与终点。要在注入点(调用 inject() 时)而非声明点放置这些配置。

skipSelf 在逻辑树上的效果

要改变 FlowerService 查找的起点,在 child.ts 中注入 FlowerService 的属性初始化处加 skipSelf

flower = inject(FlowerService, {skipSelf: true});

skipSelf 后,<app-child> 的注入器不查自身,而是从 <app-root>ElementInjector 开始找(那里什么都没有),然后回到 FlowerService 所属的应用级注入器,找到红色朱槿 🌺。UI 渲染为:

Emoji from FlowerService: 🌺

逻辑树:

<app-root @ApplicationConfig
          @Inject(FlowerService) flower=>"🌺">
  <#VIEW>
  <app-child @Provide(FlowerService="🌻" )>
    <#VIEW @Inject(FlowerService, SkipSelf)=>"🌺">

    <!-- With SkipSelf, the injector looks to the next injector up the tree (app-root) -->

  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

尽管 <app-child> 提供了向日葵 🌻,应用渲染的却是红色朱槿 🌺——skipSelf 使当前注入器(app-child)跳过自身、向上看父级。

若再加上 host(连同 skipSelf),结果是 null:因为 host 把搜索上界限制在 app-child<#VIEW>,而那里没有 FlowerService

<app-root @ApplicationConfig
          @Inject(FlowerService) flower=>"🌺">
  <#VIEW> <!-- end search here with null-->
  <app-child @Provide(FlowerService="🌻" )> <!-- start search here -->
    <#VIEW inject(FlowerService, {skipSelf: true, host: true, optional:true})=>null>
  </
  #VIEW>
  </app-parent>
</#VIEW>
</app-root>

服务与取值都没变,但 host 阻止注入器越过 <#VIEW> 继续找,查不到便返回 null

skipSelf 与 viewProviders

回忆 <app-child>viewProviders 以狗 🐶 提供了 AnimalService。注入器只需查看 <app-child>ElementInjector 就能找到 AnimalService,看不到鲸鱼 🐳。

若在 AnimalServiceinject() 上加 skipSelf,注入器就不在当前 <app-child>ElementInjector 中查找,改从 <app-root>ElementInjector 开始:

@Component({
  selector: 'app-child',
  …
  viewProviders: [
    { provide: AnimalService, useValue: { emoji: '🐶' } },
  ],
})

逻辑树:

<app-root @ApplicationConfig
          @Inject(AnimalService=>"🐳")>
  <#VIEW><!-- search begins here -->
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    @Inject(AnimalService, SkipSelf=>"🐳")>

    <!--Add skipSelf -->

  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

skipSelf 时,注入器从 <app-root>ElementInjector 开始找 AnimalService,找到鲸鱼 🐳。

host 与 viewProviders

仅用 host 注入 AnimalService,结果是狗 🐶——因为注入器在 <app-child> 自身的 <#VIEW> 里就找到了它:

@Component({
  selector: 'app-child',
  …
  viewProviders: [
    { provide: AnimalService, useValue: { emoji: '🐶' } },
  ]
})
export class Child {
  animal = inject(AnimalService, { host: true })
}

host: true 使注入器查找直至 <#VIEW> 的边界:

<app-root @ApplicationConfig
          @Inject(AnimalService=>"🐳")>
  <#VIEW>
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    inject(AnimalService, {host: true}=>"🐶")> <!-- host stops search here -->
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

接下来给 app.ts@Component() 元数据添加第三个动物——刺猬 🦔 的 viewProviders

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrls: [ './app.css' ],
  viewProviders: [
    { provide: AnimalService, useValue: { emoji: '🦔' } },
  ],
})

再把 hostskipSelf 同时加到 child.tsAnimalService 注入的 inject()

export class Child {
  animal = inject(AnimalService, {host: true, skipSelf: true});
}

对比此前 skipSelf + host 作用于 providers 数组中的 FlowerService 时结果为 nullskipSelf<app-child> 注入器开始,而 host<#VIEW> 处停止,那里没有 FlowerService——逻辑树中 FlowerService 可见于 <app-child> 而非其 <#VIEW>);此处 AnimalService 却可见,逻辑树解释了原因:

<app-root @ApplicationConfig
          @Inject(AnimalService=>"🐳")>
  <#VIEW @Provide(AnimalService="🦔")
  @Inject(AnimalService, @Optional)=>"🦔">

  <!-- ^^skipSelf starts here,  host stops here^^ -->
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    inject(AnimalService, {skipSelf:true, host: true, optional: true})=>"🦔">
    <!-- Add skipSelf ^^-->
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

skipSelf 使注入器从 <app-root> 而非发起请求的 <app-child> 开始查找 AnimalServicehost 使搜索在 <app-root><#VIEW> 处停止。由于 AnimalServiceviewProviders 提供,注入器在 <#VIEW> 中找到刺猬 🦔。

实战场景:ElementInjector 的用例

在不同层级配置 provider 打开了许多实用可能。

场景一:服务隔离

架构上的原因可能要求你把服务的访问限制在其所属的应用域内。假设构建一个显示反派列表的 VillainsList,数据来自 VillainsService

若把 VillainsService 提供在根 AppModule,它会在整个应用中可见;日后修改 VillainsService 时,可能意外破坏那些偶然依赖了它的组件。

更好的做法是把 VillainsService 提供在 VillainsListproviders 元数据中:

@Component({
  selector: 'app-villains-list',
  templateUrl: './villains-list.html',
  providers: [VillainsService],
})
export class VillainsList {}

VillainsService 只提供在 VillainsList 的元数据中,该服务就只在 VillainsList 及其子组件树中可用。它是相对 VillainsList 的单例:只要 VillainsList 不被销毁,拿到的就是同一个实例;但若有多个 VillainsList 实例,每个实例各自拥有独立的 VillainsService 实例。

场景二:多个并行的编辑会话

许多应用允许用户同时处理多个任务。例如报税应用中,报税员可能一天内在多个税表之间来回切换。

想象一个显示超级英雄列表的 HeroList:点击英雄名字打开编辑组件;每个被选中的税表在各自组件中打开,多个税表可以同时打开。每个税表组件:

  • 是独立的税表编辑会话;
  • 修改某个税表不影响其他组件中的税表;
  • 可以保存或取消对自己税表的修改。

假设 HeroTaxReturn 拥有管理与恢复修改的逻辑——对真实世界中复杂的税表数据模型而言,这项改动管理交给一个辅助服务更合适:

HeroTaxReturnService 缓存单个 HeroTaxReturn、跟踪改动并可保存/恢复,同时通过注入委托给应用级单例 HeroService

import {inject, Service} from '@angular/core';
import {HeroTaxReturn} from './hero';
import {HeroesService} from './heroes.service';

@Service({autoProvided: false})
export class HeroTaxReturnService {
  private currentTaxReturn!: HeroTaxReturn;
  private originalTaxReturn!: HeroTaxReturn;

  private heroService = inject(HeroesService);

  set taxReturn(htr: HeroTaxReturn) {
    this.originalTaxReturn = htr;
    this.currentTaxReturn = htr.clone();
  }

  get taxReturn(): HeroTaxReturn {
    return this.currentTaxReturn;
  }

  restoreTaxReturn() {
    this.taxReturn = this.originalTaxReturn;
  }

  saveTaxReturn() {
    this.taxReturn = this.currentTaxReturn;
    this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe();
  }
}

使用 HeroTaxReturnServiceHeroTaxReturn 组件:

import {Component, input, output} from '@angular/core';
import {HeroTaxReturn} from './hero';
import {HeroTaxReturnService} from './hero-tax-return.service';

@Component({
  selector: 'app-hero-tax-return',
  templateUrl: './hero-tax-return.html',
  styleUrls: ['./hero-tax-return.css'],
  providers: [HeroTaxReturnService],
})
export class HeroTaxReturn {
  message = '';

  close = output<void>();

  get taxReturn(): HeroTaxReturn {
    return this.heroTaxReturnService.taxReturn;
  }

  taxReturn = input.required<HeroTaxReturn>();

  constructor() {
    effect(() => {
      this.heroTaxReturnService.taxReturn = this.taxReturn();
    });
  }

  private heroTaxReturnService = inject(HeroTaxReturnService);

  onCanceled() {
    this.flashMessage('Canceled');
    this.heroTaxReturnService.restoreTaxReturn();
  }

  onClose() {
    this.close.emit();
  }

  onSaved() {
    this.flashMessage('Saved');
    this.heroTaxReturnService.saveTaxReturn();
  }

  flashMessage(msg: string) {
    this.message = msg;
    setTimeout(() => (this.message = ''), 500);
  }
}

待编辑的税表经 input 属性传入:setter 用传入的税表初始化组件自己的 HeroTaxReturnService 实例,getter 始终返回该服务认定的当前状态;组件也通过服务保存/恢复税表。

如果该服务是应用级单例,这一切都不成立:所有组件共享同一实例,每个组件都会覆盖属于其他英雄的税表。

为杜绝这一点,在 HeroTaxReturn 的组件级注入器中提供该服务(providers 属性):

providers: [HeroTaxReturnService];

每个组件实例都有自己的注入器,把服务提供在组件级,就保证了组件的每一个实例都拿到一个私有的服务实例——任何税表都不会被覆盖。

场景三:专用(特化)provider

在更深层级再次提供服务的另一个理由,是用更专用的实现替换上层的通用实现。

考虑一个包含轮胎服务信息、并依赖其他服务提供更多汽车详情的 Car 组件:

  1. 根注入器(A)使用 CarServiceEngineService通用 provider;
  2. 子组件(B)定义自己的专用 provider,具备适配其场景的特殊能力;
  3. (B)的子组件(C)为 CarService 定义更加专用的 provider。
graph TD;
subgraph COMPONENT_A[Component A]
subgraph COMPONENT_B[Component B]
COMPONENT_C[Component C]
end
end

style COMPONENT_A fill:#BDD7EE
style COMPONENT_B fill:#FFE699
style COMPONENT_C fill:#A9D18E,color:#000
classDef noShadow filter:none
class COMPONENT_A,COMPONENT_B,COMPONENT_C noShadow

幕后,每个组件都建立了自己的注入器,各自定义了零个、一个或多个 provider。

在最深层组件(C)解析 Car 实例时,各注入器分别产出:

  • 由注入器(C)解析的 Car 实例;
  • 由注入器(B)解析的 Engine
  • 由根注入器(A)解析的 Tires
graph BT;

subgraph A[" "]
direction LR
RootInjector["(A) RootInjector"]
ServicesA["CarService, EngineService, TiresService"]
end

subgraph B[" "]
direction LR
ParentInjector["(B) ParentInjector"]
ServicesB["CarService2, EngineService2"]
end

subgraph C[" "]
direction LR
ChildInjector["(C) ChildInjector"]
ServicesC["CarService3"]
end

direction LR
car["(C) Car"]
engine["(B) Engine"]
tires["(A) Tires"]

direction BT
car-->ChildInjector
ChildInjector-->ParentInjector-->RootInjector

class car,engine,tires,RootInjector,ParentInjector,ChildInjector,ServicesA,ServicesB,ServicesC,A,B,C noShadow
style car fill:#A9D18E,color:#000
style ChildInjector fill:#A9D18E,color:#000
style engine fill:#FFE699,color:#000
style ParentInjector fill:#FFE699,color:#000
style tires fill:#BDD7EE,color:#000
style RootInjector fill:#BDD7EE,color:#000

这正是"就近解析 + 未命中则上溯"规则的自然结果:每个 token 都由其可见范围内最近的 provider 解析,没有覆盖的 token 穿透到上层注入器。

小结与延伸阅读

分层依赖注入的关键心智模型可以浓缩为三点:

  1. 两棵树,一个兜底ElementInjector 树按元素/视图就近解析,找不到再委托 EnvironmentInjector 层级;NgModule 应用还有一层 ModuleInjector 兜底;所有查找的最终终点是 NullInjector(抛错或按 optional 返回 null)。
  2. 修饰符控制起点与终点skipSelf 改起点,self/host 定终点,optional 定失败返回值;实现上只是 R3Injector.get() 中几行位标志运算。
  3. #VIEW 是可见性边界providers 挂在组件元素上,viewProviders 挂在该组件的视图内;投影内容遵循"声明位置"而非"渲染位置"查找祖先。

相关文档:

实现侧的关键源码位置:注入选项定义 packages/core/src/di/interface/injector.ts、环境注入器查找 packages/core/src/di/r3_injector.ts、节点注入器查找 packages/core/src/render3/di.ts、树顶哨兵 packages/core/src/di/null_injector.ts

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

项目优选

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