Angular 分层依赖注入详解:注入器层级、解析规则与可见性控制
本文基于 Angular 官方指南 Hierarchical injectors 编写,系统讲解 Angular 分层依赖注入(DI)体系的完整脉络:EnvironmentInjector 与 ElementInjector 两大注入器层级、两阶段 token 解析规则、optional/self/skipSelf/host 四种解析修饰符,以及 providers 与 viewProviders 在模板逻辑树中的可见性差异。读完本文,你可以结合仓库源码(InjectOptions 接口定义、R3Injector 查找实现、NodeInjector 解析入口)精确控制服务在组件树中的可见范围,理解 #VIEW 边界与内容投影下的注入行为。
基础概念(如注入器层级与 provider 作用域的入门介绍)可参考 defining dependency providers 指南。
两大注入器层级
Angular 中存在两种注入器层级:
| 注入器层级 | 说明 |
|---|---|
EnvironmentInjector 层级 |
通过 @Service() 装饰器,或在 ApplicationConfig 的 providers 数组中配置 |
ElementInjector 层级 |
在每个 DOM 元素上隐式创建。ElementInjector 默认为空,除非你在 @Directive() 或 @Component() 的 providers 属性中为它配置了 provider |
NgModule 应用:对于基于
NgModule的应用,还可以通过@NgModule()或@Injectable()注解,使用ModuleInjector层级来提供依赖。
EnvironmentInjector:两种配置方式
EnvironmentInjector 可以通过以下两种方式配置:
@Service()装饰器ApplicationConfig的providers数组
推荐优先使用 @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属性(指向root或platform)@NgModule()的providers数组
ModuleInjector 由 @NgModule.providers 和 NgModule.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 的实现只有寥寥数行:notFoundValue 为 THROW_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的优先级:如果你在bootstrapApplication的ApplicationConfig中配置了应用级 provider,它会覆盖@Injectable()元数据中为root配置的 provider。一个典型场景是为路由配置非默认的 LocationStrategy:
providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}];
对于 NgModule 应用,则在 AppModule 的 providers 中配置应用级 provider。
ElementInjector:每个组件实例一份
Angular 会为每个 DOM 元素隐式创建 ElementInjector 层级。在 @Component() 中通过 providers 或 viewProviders 提供服务的,就是 ElementInjector:
@Component({
/* … */
providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent
在组件中提供的服务经由该组件实例的 ElementInjector 可见;依据解析规则,它也可能对子组件/指令可见。组件实例被销毁时,其服务实例也随之销毁——这是组件级 provider 的生命周期语义。
由于组件是特殊类型的指令,@Directive() 一样有 providers 属性:指令和组件都可以配置 provider,且同一元素上的组件与指令共享同一个注入器。
解析规则:两阶段查找
当为一个组件/指令解析 token 时,Angular 分两个阶段进行:
ElementInjector层级:先沿着元素注入器树的祖先向上找;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 的解析行为可用 optional、self、skipSelf 和 host 修改。它们都从 @angular/core 导入,写在 inject() 的配置参数里。其类型定义见 InjectOptions 接口:
export interface InjectOptions {
/** 使用可选注入,未找到时返回 `null`。 */
optional?: boolean;
/** 从当前注入器的父注入器开始注入。 */
skipSelf?: boolean;
/** 只查询当前注入器,不向上级联。 */
self?: boolean;
/** 在宿主组件注入器处停止注入。仅对元素注入器有意义,对环境注入器无效。 */
host?: boolean;
}
修饰符分类
解析修饰符分为三类:
- 没找到时怎么办:
optional - 从哪里开始找:
skipSelf - 到哪里停止找:
host和self
默认情况下,Angular 始终从当前注入器开始、一路向上搜索。修饰符的作用就是改变起点(self)或终点。
可任意组合,但以下两组互斥:
host与selfskipSelf与self
源码中修饰符以位标志(InternalInjectFlags,见 interface/injector.ts)编码:Host = 0b0001、Self = 0b0010、SkipSelf = 0b0100、Optional = 0b1000,这也解释了为何 self 与 skipSelf、self 与 host 在语义上互斥。
optional
optional 让 Angular 把注入的服务视为可选:若运行时解析不到,返回 null 而不是抛错。下例中 OptionalService 未在 ApplicationConfig、@NgModule() 或任何组件中提供,整个应用中都不可用:
export class Optional {
public optional? = inject(OptionalService, {optional: true});
}
self
self 让 Angular 只查看当前组件/指令的 ElementInjector。典型用途是"仅当宿主元素上恰好提供该服务时才注入它",此时应把 self 与 optional 组合以避免报错:
@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
skipSelf 是 self 的反面:查找从父 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 组合
把 skipSelf 与 optional 一起使用,可在值为 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 都用 🌷。
从源码注释看,host 与 self 的精确区别是: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 提供 FlowerService,emoji 值为红色朱槿 🌺:
@Service()
export class FlowerService {
emoji = '🌺';
}
应用只有 App 与 Child。最基础的渲染视图是嵌套的 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,分两个阶段:
- 注入器确定逻辑树中的起点与终点,从起点出发在每个视图层寻找 token;找到即返回。
- 若未找到,注入器寻找最近的父级
EnvironmentInjector委托请求。
本例中的约束是:
- 从属于
<app-root>的<#VIEW>开始,到<app-root>结束。- 通常查找起点在注入点。但此处
<app-root>是组件,@Component比较特殊——查找会包含它自己的viewProviders,所以起点是<app-root>的<#VIEW>。若同一位置匹配的是指令则不是这种情况。 - 终点恰好是组件本身,因为它是应用中最顶层的组件。
- 通常查找起点在注入点。但此处
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);
}
在 Child 与 App 模板中都添加绑定:
<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 的差异:内容投影边界
viewProviders 与 providers 概念相似,但有一个显著差异: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 加入 Child 的 imports 数组:
@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: 🐶
这四个绑定演示了 providers 与 viewProviders 的差别。狗 🐶 声明在 Child 的 <#VIEW> 内,投影内容无法看到它,投影出来的 <app-inspector> 看到的是鲸鱼 🐳。
为什么投影的 <app-inspector> 还能看到 App viewProviders 里的 🐳? 因为 Angular DI 追踪的是组件在哪里被声明,而不是它最终渲染在哪里。<app-inspector> 声明在 App 的模板里——位于 App 的 <#VIEW> 内——所以 App 的 viewProviders 对它是可达的。把它投影进 Child 只是切断了访问 Child viewProviders(🐶)的通路,但沿树上行仍然能拿到 App 的 provider(🐳)。
而下一个输出里,Inspector 是 Child 的真正子组件,位于 <#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的模板中,可以一路上行到App的viewProviders。 - 直接位于
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,看不到鲸鱼 🐳。
若在 AnimalService 的 inject() 上加 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: '🦔' } },
],
})
再把 host 与 skipSelf 同时加到 child.ts 中 AnimalService 注入的 inject():
export class Child {
animal = inject(AnimalService, {host: true, skipSelf: true});
}
对比此前 skipSelf + host 作用于 providers 数组中的 FlowerService 时结果为 null(skipSelf 从 <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> 开始查找 AnimalService,host 使搜索在 <app-root> 的 <#VIEW> 处停止。由于 AnimalService 经 viewProviders 提供,注入器在 <#VIEW> 中找到刺猬 🦔。
实战场景:ElementInjector 的用例
在不同层级配置 provider 打开了许多实用可能。
场景一:服务隔离
架构上的原因可能要求你把服务的访问限制在其所属的应用域内。假设构建一个显示反派列表的 VillainsList,数据来自 VillainsService。
若把 VillainsService 提供在根 AppModule,它会在整个应用中可见;日后修改 VillainsService 时,可能意外破坏那些偶然依赖了它的组件。
更好的做法是把 VillainsService 提供在 VillainsList 的 providers 元数据中:
@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();
}
}
使用 HeroTaxReturnService 的 HeroTaxReturn 组件:
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 组件:
- 根注入器(A)使用
CarService与EngineService的通用 provider; - 子组件(B)定义自己的专用 provider,具备适配其场景的特殊能力;
- (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 穿透到上层注入器。
小结与延伸阅读
分层依赖注入的关键心智模型可以浓缩为三点:
- 两棵树,一个兜底:
ElementInjector树按元素/视图就近解析,找不到再委托EnvironmentInjector层级;NgModule应用还有一层ModuleInjector兜底;所有查找的最终终点是NullInjector(抛错或按optional返回null)。 - 修饰符控制起点与终点:
skipSelf改起点,self/host定终点,optional定失败返回值;实现上只是 R3Injector.get() 中几行位标志运算。 #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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00