Angular 22 源码级深度解析-第 13 章 依赖注入(一):Injector 抽象、R3Injector 与 bloom filter
本章定位
前 12 章我们把"模板如何变成 DOM""状态如何驱动刷新""视图如何组织"讲完了。但有一个问题一直被绕开:组件构造函数里那个参数,是从哪儿来的?
ts@Component({...}) export class UserCard { private http = inject(HttpClient); // ← 这个 HttpClient 实例是谁造的? }这一章和下一章回答这个问题。第 13 章讲**环境注入器(EnvironmentInjector /
R3Injector)与 节点注入器(NodeInjector)**的数据结构和查找算法;第 14 章讲 provider 的编译期形态、providedIn树摇、多级注入器的层次设计与实战。本章的核心是一个算法:bloom filter。它是 Angular 能把"沿组件树向上逐级查找依赖"这件事做到近似 O(1) 的原因。我会逐位推导它的位运算,并给出一个可运行的推演。
本章定位
本章要回答的问题 :为什么 Angular 需要两套注入器?bloom filter 到底在优化什么?
viewProviders与providers的差别实现在哪一层?为什么值得单独一章:DI 是唯一一个同时被「应用启动」和「渲染热路径」使用的子系统,所以它必须同时具备精确查找(环境注入器的 records Map)与快速否定(节点注入器的 bloom filter)两套机制。
怎么读:13.2--13.10 讲环境注入器(惰性、树摇、注入上下文),13.11--13.20 讲节点注入器与 bloom filter,13.27 的手工演算建议自己推一遍。
13.1 为什么 Angular 需要"两套"注入器层级
大多数框架只有一套 DI 容器。Angular 有两套,而且它们的生命周期、作用域、数据结构完全不同。这不是历史包袱,是两个正交的需求:
| 环境注入器(Environment) | 节点注入器(Node) | |
|---|---|---|
| 代表类型 | R3Injector / EnvironmentInjector |
NodeInjector |
| 层级依据 | 注入器自己组成的父子链 | DOM/组件树 |
| 生命周期 | 与应用/路由/懒加载模块同寿 | 与单个 DOM 节点同寿 |
| 数据结构 | Map<token, Record>(哈希表) |
LView / TView.data 里的 9 个连续槽位 |
| 实例化时机 | 惰性(首次 get) | 惰性(首次 get)+ 与视图创建耦合 |
| 典型 provider | providedIn: 'root'、bootstrapApplication({providers})、路由 providers |
@Component({providers / viewProviders})、指令 providers |
| 数量 | 通常个位数 | 与元素数量同量级,可能上万个 |
关键差别在最后一行。如果一个页面有 5000 个元素,且有 200 个声明了 providers,Angular 就要维护 200 个"注入器实例"。用 Map 来做这件事,内存和 GC 压力都不可接受 ------每个 Map 在 V8 里起步就是几十字节的开销,还带隐藏类和扩容。
于是 Angular 做了一个激进的设计:把注入器压平进 LView 和 TView.data 这两个已经存在的数组里,每个注入器只占 9 个槽位,零对象分配。
而"压平进数组"带来的新问题是:注入器之间的父子关系怎么表达?Map 可以用 parent 指针,数组里怎么办?
答案是:用整数编码的"相对位置" 。父注入器被编码成一个数字,低 15 位是"父注入器在它自己 LView 里的索引",高 16 位是"要往上走几层 LView 才能找到那个 LView":
ts
// packages/core/src/render3/interfaces/injector.ts
export const enum RelativeInjectorLocationFlags {
InjectorIndexMask = 0b111111111111111, // 低 15 位:索引
ViewOffsetShift = 16, // 第 16 位起:视图偏移
NO_PARENT = -1,
}
一个 32 位整数装下"跨视图的父指针"。这就是 Angular 为了省内存能做到什么程度的直接证据。
而 bloom filter 则解决另一个问题:沿树向上查找时,如何跳过那些"肯定没有这个 token"的节点。
13.2 Injector 抽象:四个 get 重载与三个静态成员
先看最外层的抽象。packages/core/src/di/injector.ts(147 行):
ts
export abstract class Injector {
static THROW_IF_NOT_FOUND = THROW_IF_NOT_FOUND;
static NULL: Injector = /* @__PURE__ */ new NullInjector();
abstract get<T>(token: ProviderToken<T>, notFoundValue: undefined,
options: InjectOptions & {optional?: false}): T;
abstract get<T>(token: ProviderToken<T>, notFoundValue: null | undefined,
options: InjectOptions): T | null;
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
/** @deprecated from v4.0.0 use ProviderToken<T> */
abstract get<T>(token: string | ProviderToken<T>, notFoundValue?: any): any;
static create(providers: StaticProvider[], parent?: Injector): Injector;
static create(options: {providers: Array<Provider|StaticProvider>; parent?: Injector; name?: string}): DestroyableInjector;
/** @nocollapse */
static ɵprov = /* @__PURE__ */ ɵɵdefineInjectable({
token: Injector,
providedIn: 'any',
factory: () => ɵɵinject(INJECTOR),
});
/** @internal @nocollapse */
static __NG_ELEMENT_ID__ = InjectorMarkers.Injector;
}
if (typeof ngDevMode === 'undefined' || ngDevMode) {
registerSpecialProvider(Injector);
}
这段代码里有五处值得停下来看:
13.2.1 为什么有四个 get 重载
TypeScript 的重载只是类型层面的,运行时只有一个实现。这四个签名的真正作用是区分三种返回类型:
notFoundValue: undefined+optional?: false→ 返回T(找不到就抛)notFoundValue: null | undefined+ 任意 options → 返回T | null- 泛型默认 → 返回
T
第 4 个是 v4 时代 string token 的遗留,标了 @deprecated 但仍然保留------因为 Injector 是公开 API,删掉一个重载签名会破坏大量第三方类型。
13.2.2 static ɵprov:Injector 自己也是可被注入的
这一行很妙。Injector 类自己带了一个 ɵprov,providedIn: 'any',工厂是 ɵɵinject(INJECTOR)。
也就是说:
ts
class MyService {
injector = inject(Injector); // ← 能工作,因为上面这行
}
而 INJECTOR 这个 token 是在 R3Injector 构造时注册的:
ts
// r3_injector.ts, constructor
this.records.set(INJECTOR, makeRecord(undefined, this));
每个 R3Injector 都把自己注册成 INJECTOR 的值 。所以在根注入器里 inject(Injector) 拿到根注入器,在路由注入器里拿到路由注入器。这就是"注入器能注入自己"的实现,总共两行代码。
providedIn: 'any' 的意思会在 §13.7 讲------它让 Injector 在任何 作用域的注入器里都能被解析到(因为每个 R3Injector 都显式注册了它,providedIn 只是兜底)。
13.2.3 static __NG_ELEMENT_ID__ = InjectorMarkers.Injector
这是本章最重要的一个伏笔。__NG_ELEMENT_ID__ 是节点注入器的 bloom filter 用来给每个 token 分配唯一 ID 的字段。
对普通 service:
ts
@Injectable() class Foo {}
// 编译时生成:Foo.__NG_ELEMENT_ID__ = 0 (运行时由 bloomAdd 分配)
ID 是非负整数,用于 bloom filter 的哈希。
但 Injector 是特殊的------它不通过 bloom filter 查找,而是通过"直接构造一个 NodeInjector 给你"。所以它被分配一个负数 ID 作为标记:
ts
// di.ts, bloomHashBitOrFactory
const tokenId: number|undefined =
Object.hasOwn(token, NG_ELEMENT_ID) ? (token as any)[NG_ELEMENT_ID] : undefined;
if (typeof tokenId === 'number') {
if (tokenId >= 0) {
return tokenId & BLOOM_MASK;
} else {
ngDevMode && assertEqual(tokenId, InjectorMarkers.Injector, 'Expecting to get Special Injector Id');
return createNodeInjector; // ← 返回一个函数,不是数字!
}
}
这是全章最优雅的一处类型技巧 :bloomHashBitOrFactory 的返回类型是 number | Function | undefined------返回数字表示"走 bloom filter 路径",返回函数表示"这是特殊 token,直接调用这个函数拿值"。
同样的机制服务于 ElementRef、TemplateRef、ViewContainerRef、ChangeDetectorRef 等所有"与节点绑定的内建 token"。它们都不是 provider,而是由运行时现场构造的对象。
13.2.4 registerSpecialProvider(Injector) 只在 dev 模式
ts
if (typeof ngDevMode === 'undefined' || ngDevMode) {
registerSpecialProvider(Injector);
}
这个 if 的判断条件值得注意:typeof ngDevMode === 'undefined' || ngDevMode。写成这样是因为 ngDevMode 在闭包编译器/Rollup 的 dead code elimination 里会被整体替换成 false ,从而整个 if 块被删掉。但如果代码在没有定义 ngDevMode 的环境下运行(比如某些测试),typeof 检查会让它仍然生效------保守兜底。
同类写法在全仓库出现几百次,是 Angular 的标配 idiom。
13.2.5 Injector.NULL 与 NullInjector
ts
// null_injector.ts (29 行)
export class NullInjector implements Injector {
get(token: any, notFoundValue: any = THROW_IF_NOT_FOUND): any {
if (notFoundValue === THROW_IF_NOT_FOUND) {
throw new RuntimeError(
RuntimeErrorCode.PROVIDER_NOT_FOUND,
ngDevMode && `NullInjectorError: No provider for ${stringify(token)}!`);
}
return notFoundValue;
}
}
NullInjector 是责任链模式的终点。它不做任何查找,只负责"抛错或返回默认值"。
R3Injector 的 parent 链条最终都会指向它:
ts
// create_injector.ts
return new R3Injector(providers, parent || getNullInjector(), source || null, scopes);
注意 getNullInjector() 是惰性单例:
ts
let NULL_INJECTOR: Injector | undefined = undefined;
export function getNullInjector(): Injector {
if (NULL_INJECTOR === undefined) {
NULL_INJECTOR = new NullInjector();
}
return NULL_INJECTOR;
}
为什么要惰性?因为 Injector.NULL 是静态字段 ,在类初始化时就求值了(new NullInjector())。而模块顶层的 new 会阻碍 tree-shaking。惰性的这个是为了减少启动时的对象分配 ------一个 NullInjector 没有任何字段,但它仍然是一个对象。
13.3 R3Injector:records Map 与三态值机
现在钻进环境注入器的实现。packages/core/src/di/r3_injector.ts(743 行)。
13.3.1 四个字段
ts
export class R3Injector extends EnvironmentInjector implements PrimitivesInjector {
private records = new Map<ProviderToken<any>, Record<any> | null>();
private _ngOnDestroyHooks = new Set<OnDestroy>();
private _onDestroyHooks: Array<() => void> = [];
private injectorDefTypes: Set<Type<unknown>>;
private _destroyed = false;
constructor(
providers: Array<Provider | EnvironmentProviders>,
readonly parent: Injector,
readonly source: string | null,
readonly scopes: Set<InjectorScope>,
) { ... }
}
records:token → Record。null值有特殊含义,见 §13.4.3_ngOnDestroyHooks:Set,收集所有实例化出来且带ngOnDestroy的服务_onDestroyHooks:用户通过injector.onDestroy(cb)注册的回调injectorDefTypes:本注入器"拥有"的InjectorType(即providedIn: SomeModule能生效的模块集合)
13.3.2 Record:一个 Record 就是一条 provider 记录的完整状态
ts
interface Record<T> {
factory: ((_: undefined, flags?: InternalInjectFlags) => T) | undefined;
value: T | {};
multi: any[] | undefined;
}
三个字段,但 value 字段能取三种截然不同的值:
ts
/** Marker which indicates that a value has not yet been created from the factory function. */
const NOT_YET = {};
/** Marker which indicates that the factory function for a token is in the process of being called. */
const CIRCULAR = {};
value |
含义 |
|---|---|
NOT_YET |
有 factory,还没实例化 |
CIRCULAR |
factory 正在执行中(用于循环依赖检测) |
| 其他任意值 | 已实例化的真实值 |
这里有一个容易被忽略的边界 :如果一个服务的真实值恰好就是 {}(比如你 useValue: {}),那么 value === NOT_YET 就区分不出来了。
但这不会出问题,因为:
NOT_YET和CIRCULAR是模块级单例对象 ,用===比较的是引用而非结构useValue: {}会创建一个新的{}对象,与NOT_YET引用不同
所以 value === NOT_YET 永远是精确的引用比较。这是用"哨兵对象"而不是"哨兵值"的经典理由。
13.3.3 构造函数:五步
ts
constructor(providers, parent, source, scopes) {
super();
// 1. 为每个 provider 建立 Record
forEachSingleProvider(providers as Array<Provider|InternalEnvironmentProviders>, (provider) =>
this.processProvider(provider),
);
// 2. 把自己注册成 INJECTOR 的值
this.records.set(INJECTOR, makeRecord(undefined, this));
// 3. 如果是 environment scope,也注册成 EnvironmentInjector
if (scopes.has('environment')) {
this.records.set(EnvironmentInjector, makeRecord(undefined, this));
}
// 4. 检测 APP_ROOT_SCOPE
const record = this.records.get(INJECTOR_SCOPE) as Record<InjectorScope|null>;
if (record != null && typeof record.value === 'string') {
this.scopes.add(record.value as InjectorScope);
}
// 5. 收集 injectorDefTypes
this.injectorDefTypes = new Set(this.get(INJECTOR_DEF_TYPES, EMPTY_ARRAY, {self: true}));
}
第 2 步 已经在 §13.2.2 讲过。
第 3 步 是条件性的:只有 scopes 集合里含 'environment' 时才注册 EnvironmentInjector 自己。这是为了让 inject(EnvironmentInjector) 只在真正的环境注入器上成功,而不会意外拿到一个 NodeInjector 的替身。
第 4 步 处理一个历史机制:INJECTOR_SCOPE 是一个 provider token,如果它的值是 'root',那么这个注入器就自称 "root scope"。在 providedIn: 'root' 的解析里会用到(§13.7)。
第 5 步 的 {self: true} 是关键------INJECTOR_DEF_TYPES 只在本注入器的 providers 里找,不去父注入器 。否则懒加载注入器会继承祖先的所有 InjectorType,导致 providedIn: SomeModule 意外生效。
13.3.4 forEachSingleProvider:递归展平
ts
function forEachSingleProvider(
providers: Array<Provider | EnvironmentProviders>,
fn: (provider: SingleProvider) => void,
): void {
for (const provider of providers) {
if (Array.isArray(provider)) {
forEachSingleProvider(provider, fn);
} else if (provider && isEnvironmentProviders(provider)) {
forEachSingleProvider(provider.ɵproviders, fn);
} else {
fn(provider as SingleProvider);
}
}
}
三路递归:数组 → 递归;EnvironmentProviders(makeEnvironmentProviders() 的产物)→ 取 .ɵproviders 递归;否则就是单个 provider。
EnvironmentProviders 是一个品牌类型(branded type) ,它存在的唯一目的是类型层面 阻止你把环境 provider 写进 @Component({providers}):
ts
export function makeEnvironmentProviders(
providers: (Provider | EnvironmentProviders)[],
): EnvironmentProviders {
return {ɵproviders: providers} as unknown as EnvironmentProviders;
}
运行时它只是一个 {ɵproviders: [...]} 包装。但在 TS 里,@Component({providers: [...]}) 的入参类型是 Array<Provider|EnvironmentProviders> 之外的东西,写进去会编译报错。
这是一个"类型承担了运行时不该承担的职责"的设计 ------也是 makeEnvironmentProviders 存在的全部理由。它的姊妹 provider_collection.ts 里的 providerToFactory 还留着运行时兜底:
ts
if (ngDevMode && isEnvironmentProviders(provider)) {
throwInvalidProviderError(undefined, providers, provider);
}
(万一有人用 as any 绕过了类型检查。)
13.4 hydrate:惰性实例化与循环依赖检测
R3Injector.get() 找到 Record 后,调 hydrate 拿到真实值。这是整个环境注入器的核心状态机:
ts
private hydrate<T>(token: ProviderToken<T>, record: Record<T>, flags: InternalInjectFlags): T {
const prevConsumer = setActiveConsumer(null);
try {
if (record.value === CIRCULAR) {
throw cyclicDependencyError(ngDevMode ? stringify(token) : '');
} else if (record.value === NOT_YET) {
record.value = CIRCULAR;
if (ngDevMode) {
runInInjectorProfilerContext(this, token as Type<T>, () => {
emitInjectorToCreateInstanceEvent(token);
record.value = record.factory!(undefined, flags);
emitInstanceCreatedByInjectorEvent(record.value);
});
} else {
record.value = record.factory!(undefined, flags);
}
}
if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) {
this._ngOnDestroyHooks.add(record.value);
}
return record.value as T;
} finally {
setActiveConsumer(prevConsumer);
}
}
13.4.1 循环依赖检测:一个"占位符"就够了
检测算法是:
- 实例化前,把
value置为CIRCULAR - 执行 factory
- 执行完,把
value置为真实值
如果 factory 执行过程中递归地 又要注入同一个 token,第二次 hydrate 时 record.value === CIRCULAR → 抛错。
css
A 的 factory 开始 → A.value = CIRCULAR
→ 需要 B
→ B 的 factory 开始 → B.value = CIRCULAR
→ 需要 A
→ hydrate(A) 看到 CIRCULAR → 抛 CyclicDependencyError
注意"先置 CIRCULAR 再执行"的顺序是不可交换的 。如果写成"先执行再置",递归回来时 value 还是 NOT_YET,就会无限递归直到爆栈。
这个"用占位符标记'进行中'"的技巧,与第 12 章讲的 ngOnInit 的 indexWithinInitPhase 计数是同一个思想家族:用一个可比较的状态量表达"我到过这里"。
13.4.2 setActiveConsumer(null):DI 不是响应式的
第一行 const prevConsumer = setActiveConsumer(null) 与 finally 里的恢复,值得单独说。
它保证:在服务工厂函数里读 signal,不会建立依赖边。
ts
@Injectable({providedIn: 'root'})
class ThemeService {
constructor() {
const s = signal('dark');
s(); // ← 这一读,不会让 ThemeService 成为 s 的 consumer
}
}
为什么必须这样?因为服务的生命周期是单例 的,不是响应式的。如果建立依赖边,这个边会永久存在(服务不会销毁),导致信号图里挂着一堆永不释放的死边------这正是第 3 章讲的 GC 泄漏问题。
R3Injector 里有三处 setActiveConsumer(null):hydrate、destroy、resolveInjectorInitializers。凡是"由框架驱动的一次性执行",都要切断响应式上下文。
13.4.3 records.set(token, null):负缓存
回到 get() 的主流程:
ts
let record: Record<T>|undefined|null = this.records.get(token);
if (record === undefined) {
// No record, but maybe the token is scoped to this injector.
const def = couldBeInjectableType(token) && getInjectableDef(token);
if (def && this.injectableDefInScope(def)) {
record = makeRecord(injectableDefOrInjectorDefFactory(token), NOT_YET);
} else {
record = null; // ← 负缓存
}
this.records.set(token, record);
}
if (record != null /* NOT null || undefined */) {
return this.hydrate(token, record, flags);
}
undefined 和 null 在这里是两个不同的语义:
undefined= "还没查过"(Map 里没有这个 key)null= "查过了,这里没有"(负缓存)
为什么要负缓存?因为 getInjectableDef + injectableDefInScope 是一次相对昂贵 的检查(要读 ɵprov、解析 forwardRef、查 Set)。如果一个 token 在 5 层注入器链上都找不到,不做负缓存就会每层都重复做一遍。
而 if (record != null) 这个条件的注释 /* NOT null || undefined */ 是源码自带的------作者知道这里容易写成 if (record) 而出错。
13.4.4 couldBeInjectableType:先做廉价筛选
ts
function couldBeInjectableType(value: any): value is ProviderToken<any> {
return (
typeof value === 'function' ||
(typeof value === 'object' && value.ngMetadataName === 'InjectionToken')
);
}
在调 getInjectableDef 之前先做一次廉价的 typeof 检查。因为 token 可能是任意值(用户传了个字符串、数字、对象进来),直接读 .ɵprov 会触发隐藏类变化。这是 V8 优化的常见手法:先用 typeof 守住,避免多态。
InjectionToken 的识别靠 ngMetadataName === 'InjectionToken':
ts
export class InjectionToken<T> {
/** @internal */
readonly ngMetadataName = 'InjectionToken';
...
}
13.5 providerToFactory:五种 provider 形态归一为一个函数
Angular 的 provider 有五种写法,但 Record 只存一个 factory。这个归一是 providerToFactory 干的(r3_injector.ts 644 行):
ts
export function providerToFactory(
provider: SingleProvider,
ngModuleType?: InjectorType<any>,
providers?: any[],
): (type?: Type<unknown>, flags?: number) => any {
let factory: ((type?: Type<unknown>, flags?: InternalInjectFlags) => any) | undefined = undefined;
if (ngDevMode && isEnvironmentProviders(provider)) {
throwInvalidProviderError(undefined, providers, provider);
}
if (isTypeProvider(provider)) {
// 1. [FooService] ------ 裸类型
const unwrappedProvider = resolveForwardRef(provider);
return getFactoryDef(unwrappedProvider) || injectableDefOrInjectorDefFactory(unwrappedProvider);
} else {
if (isValueProvider(provider)) {
// 2. {provide: X, useValue: v}
factory = () => resolveForwardRef(provider.useValue);
} else if (isFactoryProvider(provider)) {
// 3. {provide: X, useFactory: fn, deps: [...]}
factory = () => provider.useFactory(...injectArgs(provider.deps || []));
} else if (isExistingProvider(provider)) {
// 4. {provide: X, useExisting: Y}
factory = (_, flags) =>
ɵɵinject(
resolveForwardRef(provider.useExisting),
flags !== undefined && flags & InternalInjectFlags.Optional
? InternalInjectFlags.Optional
: undefined,
);
} else {
// 5. {provide: X, useClass: C, deps?: [...]}
const classRef = resolveForwardRef(
provider && ((provider as StaticClassProvider|ClassProvider).useClass || provider.provide));
if (ngDevMode && !classRef) {
throwInvalidProviderError(ngModuleType, providers, provider);
}
if (hasDeps(provider)) {
factory = () => new classRef(...injectArgs(provider.deps));
} else {
return getFactoryDef(classRef) || injectableDefOrInjectorDefFactory(classRef);
}
}
}
return factory;
}
13.5.1 两条"优化路径"
注意第 1 种和第 5 种(无 deps 时)都走了 getFactoryDef(classRef) || injectableDefOrInjectorDefFactory(classRef),直接返回编译器生成的 ɵfac ,而不是包一层 () => new classRef()。
为什么这很重要?因为 ɵfac 是编译器生成的,它内部是:
ts
static ɵfac = function(t) { return new (t || FooService)(ɵɵinject(Bar), ɵɵinject(Baz)); };
它直接内联了依赖注入调用 。如果改成 () => new classRef(...injectArgs(deps)),deps 就得是一个运行时的数组,每次都要遍历、逐个 ɵɵinject------多一次数组分配 + 一个循环。
而 deps 数组只在用户显式 写了 deps 时才存在(老式写法 / JIT 场景)。AOT 编译的代码永远不会走这条路。
13.5.2 useExisting 的 flags 透传
ts
factory = (_, flags) =>
ɵɵinject(
resolveForwardRef(provider.useExisting),
flags !== undefined && flags & InternalInjectFlags.Optional
? InternalInjectFlags.Optional
: undefined,
);
useExisting 是"别名"------把 X 指向 Y。实现就是"注入 Y"。
这里有个细节:它只透传 Optional,不透传 Self/SkipSelf/Host。
为什么?因为 useExisting 的语义是"X 就是 Y",而 Y 的查找应该从头开始走完整流程。如果透传 SkipSelf,{provide: A, useExisting: B} + inject(A, {skipSelf: true}) 会变成"跳过当前层找 B",语义就乱了。
这是一个刻意的不对称 :Optional 是"找不到时怎么办"(结果语义),Self/SkipSelf 是"去哪儿找"(路径语义)。别名只应该继承结果语义。
13.5.3 isValueProvider 等谓词的实现
ts
export function isValueProvider(value: SingleProvider): value is ValueProvider {
return USE_VALUE in value;
}
export function isExistingProvider(value: SingleProvider): value is ExistingProvider {
return USE_EXISTING in value;
}
export function isFactoryProvider(value: SingleProvider): value is FactoryProvider {
return USE_FACTORY in value;
}
export function isTypeProvider(value: SingleProvider): value is TypeProvider {
return typeof value === 'function';
}
export function isClassProvider(value: SingleProvider): value is ClassProvider {
return USE_CLASS in value;
}
(provider_collection.ts 409--427 行)
USE_VALUE 等是用 getClosureSafeProperty 生成的属性名:
ts
const USE_VALUE = getClosureSafeProperty({useValue: getClosureSafeProperty});
为什么搞这么绕? 因为 Google 内部的闭包编译器(Closure Compiler)会重命名对象字面量的属性。如果直接写 'useValue' in value,闭包编译后 useValue 被打成 a,字符串就匹配不上了。
getClosureSafeProperty 的原理是:从一个对象字面量里取出被打过码的属性名,这样无论编译器怎么重命名,取出来的名字和实际属性名总是一致的。
这是 Angular 为了兼容内部构建工具付出的代价之一,在全仓库出现了几十次。
13.6 multi: true------R3Injector 层的实现
multi provider 的处理在 processProvider 里(r3_injector.ts 478 行):
ts
private processProvider(provider: SingleProvider): void {
provider = resolveForwardRef(provider);
let token: any = isTypeProvider(provider) ? provider : resolveForwardRef(provider && provider.provide);
const record = providerToRecord(provider);
// ... dev-mode profiler events ...
if (!isTypeProvider(provider) && provider.multi === true) {
let multiRecord = this.records.get(token);
if (multiRecord) {
if (ngDevMode && multiRecord.multi === undefined) {
throwMixedMultiProviderError(); // ← 普通 provider 和 multi provider 混用
}
} else {
multiRecord = makeRecord(undefined, NOT_YET, true);
multiRecord.factory = () => injectArgs(multiRecord!.multi!);
this.records.set(token, multiRecord);
}
token = provider; // ← 关键!token 变成了 provider 对象本身
multiRecord.multi!.push(provider);
} else {
if (ngDevMode) {
const existing = this.records.get(token);
if (existing && existing.multi !== undefined) {
throwMixedMultiProviderError();
}
}
}
this.records.set(token, record);
}
13.6.1 那行 token = provider 是理解 multi 的钥匙
最后 this.records.set(token, record) 把每一个 multi provider 自己也注册成一条 record。
也就是说 {provide: TOKEN, useValue: 'a', multi: true} 这个对象字面量本身成了 records 里的一个 key。
为什么要这样?
因为 multiRecord.factory = () => injectArgs(multiRecord.multi!)。injectArgs 会对数组里每一项调 ɵɵinject(item):
ts
export function injectArgs(types: (ProviderToken<any>|any[]|undefined)[]): any[] {
const args: any[] = [];
for (let i = 0; i < types.length; i++) {
const arg = resolveForwardRef(types[i]);
if (Array.isArray(arg)) {
...
} else {
args.push(ɵɵinject(arg));
}
}
return args;
}
它注入的是 provider 对象 ,不是 TOKEN。所以 records 里必须能用 provider 对象做 key 查到它的 record,才能拿到 providerToRecord(provider) 造出来的 factory。
这是一个用"对象身份"做 key 的技巧------provider 字面量天然唯一,不需要额外的 ID 分配。
13.6.2 multi 记录的实例化时机
yaml
multiRecord: { factory: () => injectArgs(multi), value: NOT_YET, multi: [p1, p2, p3] }
↓
injectArgs 逐个 ɵɵinject(pi)
↓
records.get(pi) → { factory: providerToFactory(pi), value: NOT_YET }
↓
hydrate → 真实值
所以 multi 的每一项都是独立的 Record ,都能单独 hydrate、单独被 ngOnDestroy 收集。
13.6.3 混用检查
ts
providers: [
{provide: TOKEN, useValue: 'a'}, // 非 multi
{provide: TOKEN, useValue: 'b', multi: true}, // multi
]
会抛 throwMixedMultiProviderError()。这个检查是双向 的:先有 non-multi 再加 multi 会抛(第一个 if),先有 multi 再加 non-multi 也会抛(else 分支)。
而且它只在 ngDevMode------生产构建里这两种写法会静默地以后写的为准。
13.7 providedIn 与 injectableDefInScope:tree-shakable provider 的判定
providedIn 是 Angular DI 最有特色的机制。它的实现只有 11 行:
ts
private injectableDefInScope(def: ɵɵInjectableDeclaration<any>): boolean {
if (!def.providedIn) {
return false;
}
const providedIn = resolveForwardRef(def.providedIn);
if (typeof providedIn === 'string') {
return providedIn === 'any' || this.scopes.has(providedIn);
} else {
return this.injectorDefTypes.has(providedIn);
}
}
三种情况:
providedIn 的值 |
解析方式 |
|---|---|
'root' |
scopes.has('root') |
'platform' |
scopes.has('platform') |
'any' |
恒 true(短路在第一个条件) |
SomeNgModule(类型) |
injectorDefTypes.has(SomeNgModule) |
undefined / null |
false |
13.7.1 'any' 为什么直接为 true
providedIn: 'any' 的语义是"每个注入器一份实例"。所以它在任何注入器里都"在作用域内"。
这个选项已经标了 @deprecated(injection_token.ts 的注释里明确写了 "The providedIn: NgModule and providedIn: 'any' options are deprecated"),但 Injector 自己还在用它(§13.2.2)。
13.7.2 injectableDefOrInjectorDefFactory:三级回退
ts
function injectableDefOrInjectorDefFactory(token: ProviderToken<any>): FactoryFn<any> {
// 1. 优先用 ɵprov(@Injectable/@Service 生成的)
const injectableDef = getInjectableDef(token);
const factory = injectableDef !== null ? injectableDef.factory : getFactoryDef(token);
if (factory !== null) {
return factory;
}
// 2. InjectionToken 缺 ɵprov 是错误
if (token instanceof InjectionToken) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_INJECTION_TOKEN,
ngDevMode && `Token ${stringify(token)} is missing a ɵprov definition.`,
);
}
// 3. 无装饰器的类:零参才能造
if (token instanceof Function) {
return getUndecoratedInjectableFactory(token);
}
throw new RuntimeError(RuntimeErrorCode.INVALID_INJECTION_TOKEN, ngDevMode && 'unreachable');
}
第 3 级的 getUndecoratedInjectableFactory 有个有趣的处理:
ts
function getUndecoratedInjectableFactory(token: Function) {
const paramLength = token.length;
if (paramLength > 0) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_INJECTION_TOKEN,
ngDevMode && `Can't resolve all parameters for ${stringify(token)}: (${newArray(paramLength, '?').join(', ')}).`,
);
}
const inheritedInjectableDef = getInheritedInjectableDef(token);
if (inheritedInjectableDef !== null) {
return () => inheritedInjectableDef.factory(token as Type<any>);
} else {
return () => new (token as Type<any>)();
}
}
token.length是 JS 函数的形参个数 (Function.prototype.length)。用这个来判断"有没有构造参数"- 如果父类有
ɵprov,用父类的 factory(继承) - 否则
new token()
错误信息里 newArray(paramLength, '?').join(', ') 会生成 Can't resolve all parameters for Foo: (?, ?)------这个 ? 就是未知参数的占位。
13.7.3 树摇是怎么发生的
providedIn 的树摇价值在于:@Injectable({providedIn: 'root'}) 不需要出现在任何 providers 数组里。
ini
产物里:FooService.ɵprov = {token: FooService, providedIn: 'root', factory: ...}
Rollup/esbuild 看到:
FooService被某处inject(FooService)引用 → 保留- 没人引用 → 整个类连同 ɵprov 一起删掉
而如果是 providers: [FooService],那么 providers 数组必然 持有 FooService 的引用,无论用不用都得保留。
这就是 providedIn 的全部价值:让"是否保留这段代码"的决定权从"配置数组"转移到"引用图"上。
13.8 inject() 的实现切换:inject_switch.ts
Angular 有两个 inject 实现,运行时动态切换。这是全仓库最"脏"但最必要的一处全局可变状态。
ts
// di/inject_switch.ts
let _injectImplementation:
| (<T>(token: ProviderToken<T>, flags?: InternalInjectFlags) => T | null)
| undefined;
export function getInjectImplementation() { return _injectImplementation; }
export function setInjectImplementation(
impl: (<T>(token: ProviderToken<T>, flags?: InternalInjectFlags) => T | null) | undefined,
): (<T>(token: ProviderToken<T>, flags?: InternalInjectFlags) => T | null) | undefined {
const previous = _injectImplementation;
_injectImplementation = impl;
return previous;
}
13.8.1 为什么需要切换
源码注释给了两个理由:
markdown
* By default, it is `injectInjectorOnly`, which makes it `Injector`-only aware. It can be changed
* to `directiveInject`, which brings in the `NodeInjector` system of ivy. It is designed this
* way for two reasons:
* 1. `Injector` should not depend on ivy logic.
* 2. To maintain tree shake-ability we don't want to bring in unnecessary code.
理由 1 是架构分层 :di/injector.ts 不应该 import render3/di.ts。如果 inject() 直接调用 getOrCreateInjectable,依赖就反了。
理由 2 是包体积 :如果你的应用只用 Injector.create() 手动创建注入器,从不用组件,那么 render3/di.ts(bloom filter 那一整套)根本不该进 bundle。用运行时开关代替静态 import,让 linker/打包器能把它摇掉。
13.8.2 切换发生在哪里
ɵɵdirectiveInject 是节点注入路径的入口:
ts
// render3/instructions/di.ts
export function ɵɵdirectiveInject<T>(token: ProviderToken<T>, flags = InternalInjectFlags.Default): T|null {
const lView = getLView();
if (lView === null) {
ngDevMode && assertInjectImplementationNotEqual(ɵɵdirectiveInject);
return ɵɵinject(token, flags);
}
const tNode = getCurrentTNode();
const value = getOrCreateInjectable<T>(tNode as TDirectiveHostNode, lView, resolveForwardRef(token), flags);
ngDevMode && emitInjectEvent(token as Type<unknown>, value, flags);
return value;
}
而切换的时机在 getNodeInjectable 里:
ts
const previousInjectImplementation = factory.injectImpl
? setInjectImplementation(factory.injectImpl)
: null;
也就是说:当 Angular 正在实例化一个"声明在节点注入器里"的服务时 ,会把 inject 切换成 ɵɵdirectiveInject。这样这个服务的工厂函数里的 ɵɵinject(Dep) 就会走节点注入路径。
实例化完成后在 finally 里恢复:
ts
previousInjectImplementation !== null && setInjectImplementation(previousInjectImplementation);
13.8.3 一个反直觉的推论
ts
@Component({
providers: [LoggerA],
viewProviders: [LoggerB],
})
class MyCmp {
// LoggerA 的构造函数里 inject(LoggerB) → 拿不到!
}
因为 LoggerA 的 NodeInjectorFactory 是 new NodeInjectorFactory(providerFactory, isViewProvider=false, ...),canSeeViewProviders = false。所以 getNodeInjectable 里:
ts
const previousIncludeViewProviders = setIncludeViewProviders(factory.canSeeViewProviders);
把全局的 includeViewProviders 设成 false,searchTokensOnInjector 里的 canAccessViewProviders 就变成 false,locateDirectiveOrProvider 的 startingIndex 跳过 viewProviders 区间。
这不是靠注入器层级实现的,是靠一个全局布尔量 。这就是 di.ts 顶部那个超长注释讲的:
ts
/**
* Defines if the call to `inject` should include `viewProviders` in its resolution.
* ...
* @Component({
* providers: [MyService, {provide: String, value: 'providers'}]
* viewProviders: [{provide: String, value: 'viewProviders'}]
* })
* class MyComponent {
* constructor(myService: MyService, value: String) {
* expect(value).toEqual('viewProviders'); // 组件自己能看 viewProviders
* expect(myService.value).toEqual('providers'); // MyService 不能
* }
* }
*/
let includeViewProviders = true;
这个例子值得反复读:"能不能看见 viewProviders" 是跟着"你被声明在哪儿"走的,不是跟着"你被注入到哪儿"走。
13.8.4 injectRootLimpMode:没有注入器时的兜底
ts
export function injectRootLimpMode<T>(
token: ProviderToken<T>, notFoundValue: T|undefined, flags: InternalInjectFlags): T|null {
const injectableDef: ɵɵInjectableDeclaration<T>|null = getInjectableDef(token);
if (injectableDef && injectableDef.providedIn == 'root') {
return injectableDef.value === undefined
? (injectableDef.value = injectableDef.factory())
: injectableDef.value;
}
if (flags & InternalInjectFlags.Optional) return null;
if (notFoundValue !== undefined) return notFoundValue;
throwProviderNotFoundError(token, typeof ngDevMode !== 'undefined' && ngDevMode ? 'Injector' : '');
}
"Limp mode"(跛行模式)指应用还没 bootstrap,没有任何注入器 的状态。典型场景是单元测试里在 TestBed 初始化前调 inject。
此时只支持 providedIn: 'root',而且值缓存在 injectableDef.value 上 ------直接写进 ɵprov 对象。
注意这里的缓存在生产环境有个副作用:这个实例不会被任何注入器管理 ,ngOnDestroy 永远不会被调用,也不会因为 TestBed.resetTestingModule() 而清除。
13.9 注入上下文:setCurrentInjector 与 runInInjectionContext
13.9.1 第二个全局可变状态
ts
// di/injector_compatibility.ts (简化)
let _currentInjector: PrimitivesInjector | undefined | null = undefined;
export function setCurrentInjector(injector: PrimitivesInjector | undefined | null) {
const former = _currentInjector;
_currentInjector = injector;
return former;
}
export function getCurrentInjector() { return _currentInjector; }
所以"注入上下文"实际上是两个全局变量:
_injectImplementation(§13.8)------决定走哪条查找路径_currentInjector------当前注入器实例
injectInjectorOnly 是默认实现:
ts
export function injectInjectorOnly<T>(token: ProviderToken<T>, flags = InternalInjectFlags.Default): T|null {
const currentInjector = getCurrentInjector();
if (currentInjector === undefined) {
throw new RuntimeError(
RuntimeErrorCode.MISSING_INJECTION_CONTEXT,
ngDevMode && `The \`${stringify(token)}\` token injection failed. \`inject()\` function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with \`runInInjectionContext\`.`,
);
} else if (currentInjector === null) {
return injectRootLimpMode(token, undefined, flags);
} else {
const options = convertToInjectOptions(flags);
const value = currentInjector.retrieve(token as PrimitivesInjectionToken<T>, options) as T;
ngDevMode && emitInjectEvent(token as Type<unknown>, value, flags);
if (isNotFound(value)) {
if (options.optional) return null;
throw value;
}
return value;
}
}
三态:
undefined→ 没有上下文,抛MISSING_INJECTION_CONTEXTnull→ limp mode- 否则 → 正常查找
13.9.2 RetrievingInjector:适配器
_currentInjector 的类型是 PrimitivesInjector(@angular/core/primitives/di),接口是 retrieve(token, options)。但用户传进来的可能是任意 Injector(只有 get)。所以需要适配器:
ts
export class RetrievingInjector implements PrimitivesInjector {
constructor(readonly injector: Injector) {}
retrieve<T>(token: PrimitivesInjectionToken<T>, options: unknown): T | NotFound {
const flags: InternalInjectFlags = convertToBitFlags(options as InjectOptions|undefined) || InternalInjectFlags.Default;
try {
return (this.injector as BackwardsCompatibleInjector).get(
token as unknown as InjectionToken<T>,
(flags & InternalInjectFlags.Optional ? null : THROW_IF_NOT_FOUND) as T,
flags,
) as T;
} catch (e: any) {
if (isNotFound(e)) return e;
throw e;
}
}
}
catch 里 isNotFound(e) 的判断是为了把"抛出的 NotFound 错误"转换成"返回值"------因为 primitives 层的协议是用返回值表达 not found ,而公开 Injector.get 的协议是抛异常。
13.9.3 runInInjectionContext
ts
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT {
let internalInjector: PrimitivesInjector;
if (injector instanceof R3Injector) {
assertNotDestroyed(injector);
internalInjector = injector;
} else {
internalInjector = new RetrievingInjector(injector);
}
let prevInjectorProfilerContext: InjectorProfilerContext;
if (ngDevMode) {
prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null});
}
const prevInjector = setCurrentInjector(internalInjector);
const previousInjectImplementation = setInjectImplementation(undefined);
try {
return fn();
} finally {
setCurrentInjector(prevInjector);
ngDevMode && setInjectorProfilerContext(prevInjectorProfilerContext!);
setInjectImplementation(previousInjectImplementation);
}
}
三件事:
R3Injector直接用(它本身就 implementsPrimitivesInjector),否则包RetrievingInjectorsetInjectImplementation(undefined)------ 强制切回injectInjectorOnlytry/finally恢复
第 2 点很重要:runInInjectionContext 的语义是"在这个环境注入器的上下文里跑",所以它故意关掉 节点注入路径。否则如果你在组件构造函数里调 runInInjectionContext(rootInjector, () => inject(X)),_injectImplementation 还是 directiveInject,就会走节点路径------与 API 承诺不符。
13.9.4 isInInjectionContext 的两个条件
ts
export function isInInjectionContext(): boolean {
return getInjectImplementation() !== undefined || getCurrentInjector() != null;
}
注意是或 不是且。因为在节点注入路径下,_currentInjector 可能是 undefined(getOrCreateInjectable 不设它),但 _injectImplementation 是 directiveInject。
而 assertInInjectionContext:
ts
export function assertInInjectionContext(debugFn: Function): void {
// from being retained in the bundle regardless of minification.
if (!isInInjectionContext()) {
throw new RuntimeError(
RuntimeErrorCode.MISSING_INJECTION_CONTEXT,
ngDevMode && debugFn.name + '() can only be used within an injection context ...',
);
}
}
源码注释解释了为什么传 Function 而不是 string:传字符串会把这个字符串常量留在 bundle 里 (即使 minify 也去不掉,因为它是数据不是标识符)。传函数则可以用 debugFn.name,minify 后函数名会被压缩。
13.10 injectAsync:v22 新增的异步注入
packages/core/src/di/inject_async.ts(145 行),标注 @publicApi 22.0------这是 Angular 22 的新 API。
ts
export function injectAsync<T>(
loader: () => Promise<InjectAsyncLoaderResult<T>>,
options?: InjectAsyncOptions,
): () => Promise<T> {
if (ngDevMode) {
assertInInjectionContext(injectAsync);
}
const injector = inject(Injector);
let loadedPromise: Promise<InjectAsyncLoaderResult<T>> | null = null;
const load = () => {
if (!loadedPromise) {
loadedPromise = loader();
}
return loadedPromise;
};
if (options?.prefetch) {
options.prefetch().then(() => load()).catch(() => {});
}
return () => load().then((loadedToken) => injector.get(maybeUnwrapDefaultExport(loadedToken))!);
}
13.10.1 为什么需要它
传统写法:
ts
class MyCmp {
async onClick() {
const {HeavyService} = await import('./heavy.service');
const svc = inject(HeavyService); // ← 报错!await 之后没有注入上下文
}
}
await 之后,调用栈已经不在组件的构造期,_currentInjector 早被恢复成 undefined。
injectAsync 的解法:在同步阶段先把 Injector 抓在手里 ,异步回来后再用 injector.get()(而不是 inject())。
ts
class MyCmp {
someSvc = injectAsync(() => import('../heavy.service'));
async onClick() {
(await this.someSvc()).handleClick();
}
}
13.10.2 三个细节
injector在闭包里捕获,不是每次取。这是能在 async 后工作的唯一原因loadedPromise记忆化 ------多次调用只import()一次maybeUnwrapDefaultExport------兼容export default class的模块
13.10.3 prefetch 与 onIdle
ts
export function onIdle(options?: {timeout?: number}): Promise<void> {
if (ngDevMode) {
assertInInjectionContext(onIdle);
}
const idleService = inject(IDLE_SERVICE);
const {promise, resolve} = promiseWithResolvers<void>();
idleService.requestOnIdle(() => resolve(), options);
return promise;
}
IDLE_SERVICE 就是第 07 章讲的 @defer 用的那个 idle 调度器(defer/idle_service.ts)。这是同一套基础设施在两个场景的复用 ------@defer(on idle) 和 injectAsync(..., {prefetch: onIdle}) 共享同一个 requestIdleCallback 封装。
用法:
ts
someSvc = injectAsync(() => import('./heavy'), {prefetch: onIdle});
注意 options.prefetch() 返回的 promise 被 .catch(() => {}) 吞掉了------预取失败不应该影响正常路径。
13.11 NodeInjector 的内存布局:9 个槽位
现在进入本章的算法核心。先看数据结构。
packages/core/src/render3/interfaces/injector.ts 有一段 100 行的注释,是理解整个节点注入器的第一手资料:
markdown
* Each injector is saved in 9 contiguous slots in `LView` and 9 contiguous slots in
* `TView.data`. This allows us to store information about the current node's tokens (which
* can be shared in `TView`) as well as the tokens of its ancestor nodes (which cannot be
* shared, so they live in `LView`).
*
* `LView` layout:
* index + 0..7: cumulative bloom filter (8 个 32 位整数 = 256 bit)
* index + 8 (PARENT): Index to the parent injector. See `RelativeInjectorLocation`
*
* `TViewData` layout:
* index + 0..7: node bloom filter (8 个 32 位整数)
* index + 8 (TNODE): TNode associated with this `NodeInjector`
ts
export const enum NodeInjectorOffset {
TNODE = 8,
PARENT = 8,
BLOOM_SIZE = 8,
SIZE = 9,
}
13.11.1 为什么是两份 bloom filter
这是全章最需要想清楚的一点。
| 位置 | 内容 | 共享性 | |
|---|---|---|---|
| node bloom | TView.data |
只含本节点的 token | 所有实例共享(T 层) |
| cumulative bloom | LView |
本节点 + 所有祖先的 token | 每实例一份(L 层) |
为什么要两份?
设想你要找 token X,从节点 N 开始向上:
css
N → P → GP → ... → root
- 在 N 这一层,你要问:"N 自己有没有 X?" → 查 node bloom (
tView.data) - 查完发现没有,你要问:"还要不要继续往上?" → 查 cumulative bloom (
lView)
cumulative bloom 的作用是提前终止 :如果 N 的累积 bloom 里没有 X 的位,那么 N 的所有祖先也都没有,可以直接放弃整条链。
如果没有 cumulative bloom,你就得一路 while 到根,每层做一次 node bloom 检查------O(depth)。有了它,一次位运算就能剪掉整棵子树。
13.11.2 cumulative bloom 的构造
ts
// getOrCreateNodeInjectorForNode, di.ts 193
export function getOrCreateNodeInjectorForNode(
tNode: TElementNode|TContainerNode|TElementContainerNode, lView: LView,
): number {
const existingInjectorIndex = getInjectorIndex(tNode, lView);
if (existingInjectorIndex !== -1) {
return existingInjectorIndex;
}
const tView = lView[TVIEW];
if (tView.firstCreatePass) {
tNode.injectorIndex = lView.length;
insertBloom(tView.data, tNode); // foundation for node bloom
insertBloom(lView, null); // foundation for cumulative bloom
insertBloom(tView.blueprint, null);
}
const parentLoc = getParentInjectorLocation(tNode, lView);
const injectorIndex = tNode.injectorIndex;
if (hasParentInjector(parentLoc)) {
const parentIndex = getParentInjectorIndex(parentLoc);
const parentLView = getParentInjectorView(parentLoc, lView);
const parentData = parentLView[TVIEW].data as any;
for (let i = 0; i < NodeInjectorOffset.BLOOM_SIZE; i++) {
lView[injectorIndex + i] = parentLView[parentIndex + i] | parentData[parentIndex + i];
}
}
lView[injectorIndex + NodeInjectorOffset.PARENT] = parentLoc;
return injectorIndex;
}
那一行位或是全部魔法:
ts
lView[injectorIndex + i] = parentLView[parentIndex + i] | parentData[parentIndex + i];
子的 cumulative = 父的 cumulative | 父的 node bloom。归纳一下就是:子的 cumulative = 自己 + 父 + 祖父 + ... 一路或上去。
注意这里只 or 了父的两份,没有 or 自己的 node bloom 。自己的 token 是在 bloomAdd 时写进 tView.data(node bloom)的,而查自己的时候会单独查 tView.data(见 §13.14)。所以 cumulative 的语义严格是"祖先的 token 集合"。
13.11.3 insertBloom 的 footer 参数
ts
function insertBloom(arr: any[], footer: TNode | null): void {
arr.push(0, 0, 0, 0, 0, 0, 0, 0, footer);
}
8 个 0 + 一个 footer。footer 在 tView.data 里是 TNode(第 9 槽 = TNODE),在 lView 里是 null(第 9 槽 = PARENT,稍后被覆盖成 parentLoc)。
注意 tView.blueprint 也调了 insertBloom ------因为 blueprint 是 tView.data 的副本模板,每创建一个 LView 就 blueprint.slice()。如果 blueprint 少了这 9 个槽,后续 LView 的索引就全错了。
13.11.4 三个数组必须同步 push
resolveProvider(di_setup.ts)里每次 push token 时,必须同时推三个数组:
ts
tInjectables.push(token); // TView.data ------ token
lInjectablesBlueprint.push(factory); // blueprint ------ factory 模板
lView.push(factory); // 当前 LView ------ factory 实例
这就是为什么第 09 章强调"TView.data[i] ↔ LView[28+i] 严格对齐"。在 DI 这里,这个对齐是硬性的 :locateDirectiveOrProvider 在 tView.data 里找到索引 i,getNodeInjectable 就用同一个 i 去 lView 里取值。
13.12 bloom filter:BLOOM_SIZE=256 与 5 位桶偏移
现在讲算法本身。
13.12.1 三个常量
ts
const BLOOM_SIZE = 256;
const BLOOM_MASK = BLOOM_SIZE - 1; // 255 = 0b11111111
const BLOOM_BUCKET_BITS = 5;
let nextNgElementId = 0;
BLOOM_SIZE = 256:一个 bloom filter 共 256 个 bit- JS 的位运算是 32 位的,所以 256 bit = 8 个 32 位整数 (这就是
BLOOM_SIZE = 8槽位的来源) BLOOM_BUCKET_BITS = 5:因为2^5 = 32,一个桶表示 32 个 bitnextNgElementId:全局自增 ID 计数器
13.12.2 bloomAdd:把 token 注册进 bloom
ts
export function bloomAdd(
injectorIndex: number, tView: TView, type: ProviderToken<any>|string,
): void {
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
let id: number|undefined;
if (typeof type === 'string') {
id = type.charCodeAt(0) || 0;
} else if (Object.hasOwn(type, NG_ELEMENT_ID)) {
id = (type as any)[NG_ELEMENT_ID];
}
// Set a unique ID on the directive type
if (id == null) {
id = (type as any)[NG_ELEMENT_ID] = nextNgElementId++;
}
const bloomHash = id & BLOOM_MASK; // ① 取模 256
const mask = 1 << bloomHash; // ② 造位掩码
(tView.data as number[])[injectorIndex + (bloomHash >> BLOOM_BUCKET_BITS)] |= mask; // ③ 写桶
}
① id & BLOOM_MASK :等价于 id % 256,但位与更快。id 从 0 开始自增,第 256 个 token 会拿到 id=256,& 255 后回到 0------这就是假阳性的来源(第 256 个和第 0 个 token 映射到同一个 bit)。
② 1 << bloomHash :如果 bloomHash = 200,那么 1 << 200 在 JS 里是什么?
JS 的移位运算会先把右操作数 mod 32:
js
1 << 200 // 等价于 1 << (200 % 32) = 1 << 8 = 256
所以 mask 永远是 2^0 ~ 2^31 之一。这不会出错,因为桶的选择也用了同一个 bloomHash。
③ bloomHash >> BLOOM_BUCKET_BITS :200 >> 5 = 6。所以写到第 6 个桶。
结合起来:bloomHash = 200 → 桶 6,掩码 1 << 8。 验证:桶 6 覆盖 bit 192--223,192 + 8 = 200。✓
这是一个自洽的编码 :bloomHash 的低 5 位(0--31)决定桶内位置,高 3 位(0--7)决定桶号。5+3 = 8 bit = 256。
13.12.3 字符串 token 用 charCodeAt(0)
ts
if (typeof type === 'string') {
id = type.charCodeAt(0) || 0;
}
字符串 token(老式 @Inject('someString'))用首字符的 charCode 作为 ID。
这意味着 'apiUrl' 和 'apiKey' 会碰撞 (都是 'a' = 97)。这是可接受的------字符串 token 已经废弃,而且碰撞只会导致"多查一次",不会出错。
|| 0 是防 charCodeAt(0) 返回 NaN(空字符串)。
13.12.4 Object.hasOwn 而不是 in
ts
} else if (Object.hasOwn(type, NG_ELEMENT_ID)) {
id = (type as any)[NG_ELEMENT_ID];
}
源码注释在 bloomHashBitOrFactory 里解释了:
ts
为什么不能继承? 因为类继承会把父类的 __NG_ELEMENT_ID__ 带下来:
ts
class Base {}
Base.__NG_ELEMENT_ID__ = 5;
class Child extends Base {}
// Child.__NG_ELEMENT_ID__ 通过原型链也是 5
如果 Child 继承了 Base 的 ID,那么注入 Child 时会去查 ID=5 的位------查到的是 Base 。所以用 Object.hasOwn 强制每个类自己分配 ID。
13.12.5 什么时候分配 ID
注意 if (id == null) { id = (type as any)[NG_ELEMENT_ID] = nextNgElementId++; }。
ID 是在第一次 bloomAdd 这个 token 时分配的,不是编译期。
这意味着:同一个 token 在不同页面/不同顺序下可能拿到不同的 ID 。这没问题,因为 bloom filter 的读写用的是同一个 NG_ELEMENT_ID 字段,只要一致就行。
但这也意味着:token 的 ID 是全局的、跨 TView 共享的 。100 个组件都提供 FooService,它的 ID 只有一份。
13.13 bloomHashBitOrFactory:三种返回值
ts
export function bloomHashBitOrFactory(
token: ProviderToken<any>|string,
): number|Function|undefined {
ngDevMode && assertDefined(token, 'token must be defined');
if (typeof token === 'string') {
return token.charCodeAt(0) || 0;
}
const tokenId: number|undefined =
Object.hasOwn(token, NG_ELEMENT_ID) ? (token as any)[NG_ELEMENT_ID] : undefined;
if (typeof tokenId === 'number') {
if (tokenId >= 0) {
return tokenId & BLOOM_MASK;
} else {
ngDevMode && assertEqual(tokenId, InjectorMarkers.Injector, 'Expecting to get Special Injector Id');
return createNodeInjector;
}
} else {
return tokenId; // undefined
}
}
三种返回值,对应三条完全不同的路径:
| 返回 | 场景 | 后续 |
|---|---|---|
number |
普通 token(有 __NG_ELEMENT_ID__) |
走 bloom filter 查找(§13.15) |
Function |
特殊 token(Injector 等负数 ID) |
直接调用这个函数拿值 |
undefined |
token 从没被 bloomAdd 过 |
直接 fallback 到模块注入器 |
13.13.1 第三条路径:undefined 的短路
ts
} else {
return tokenId; // undefined
}
在 lookupTokenUsingNodeInjector 里:
ts
const bloomHash = bloomHashBitOrFactory(token);
if (typeof bloomHash === 'function') {
...
} else if (typeof bloomHash === 'number') {
... // 走 bloom 查找
}
return notFoundValue; // ← undefined 时直接落到这里
如果 token 从没被任何节点注入器提供过,bloomHash 就是 undefined,整棵节点树一次都不查,直接跳到模块注入器。
这是一个重要的性能特性:
ts
@Injectable({providedIn: 'root'}) class ApiService {}
@Component({template: `...`})
class Cmp {
api = inject(ApiService); // ← 从未在 providers 里出现过
}
ApiService 没有 __NG_ELEMENT_ID__(因为它从没被 bloomAdd),所以 bloomHash === undefined,零次位运算 就跳到 R3Injector。
这是"root service 的注入在深组件树里也很便宜"的原因。
13.13.2 特殊 token 的执行路径
ts
if (typeof bloomHash === 'function') {
if (!enterDI(lView, tNode, flags)) {
return flags & InternalInjectFlags.Host
? notFoundValueOrThrow<T>(notFoundValue, token, flags)
: lookupTokenUsingModuleInjector<T>(lView, token, flags, notFoundValue);
}
try {
let value: unknown;
if (ngDevMode) {
runInInjectorProfilerContext(
new NodeInjector(getCurrentTNode() as TElementNode, getLView()),
token as Type<T>,
() => {
emitInjectorToCreateInstanceEvent(token);
value = bloomHash(flags);
emitInstanceCreatedByInjectorEvent(value);
},
);
} else {
value = bloomHash(flags);
}
if (value == null && !(flags & InternalInjectFlags.Optional)) {
throwProviderNotFoundError(token);
} else {
return value;
}
} finally {
leaveDI();
}
}
bloomHash(flags) 就是 createNodeInjector(flags):
ts
export function createNodeInjector(): Injector {
return new NodeInjector(getCurrentTNode()! as TDirectiveHostNode, getLView()) as any;
}
所以 inject(Injector) 在节点注入器里,就是现场 new 一个 NodeInjector(tNode, lView)。
每次注入都创建新对象------这解释了为什么不建议频繁 inject(Injector) 然后 .get():每次都分配。
13.13.3 enterDI / leaveDI
这两个函数维护 DI 的"当前节点"状态(第 09 章讲过的 state.ts)。它们返回 false 表示"不能进入 DI"------典型原因是 @Host 标志下已经越过了宿主边界。
13.14 bloomHasToken 与假阳性
ts
export function bloomHasToken(
bloomHash: number, injectorIndex: number, injectorView: LView|TData,
) {
const mask = 1 << bloomHash;
const value = injectorView[injectorIndex + (bloomHash >> BLOOM_BUCKET_BITS)];
return !!(value & mask);
}
与 bloomAdd 完全对称。两次位运算:一次算桶偏移,一次与掩码。
13.14.1 假阳性(false positive)
bloom filter 的经典性质:说"没有"一定没有,说"有"可能有。
原因:
- 不同 token 的
id & 255可能相同(每 256 个循环一次) - 即使 ID 不同,多个 token 的位被 or 进同一个桶后无法区分是谁
假阳性的处理在 lookupTokenUsingNodeInjector 里:
ts
if (bloomHasToken(bloomHash, injectorIndex, tView.data)) {
const instance: T|{}|null = searchTokensOnInjector<T>(...);
if (instance !== NOT_FOUND) {
return instance;
}
}
// 没找到 → 继续往上
bloom 说"有",就去 searchTokensOnInjector 做线性扫描确认。扫不到就当没有,继续向上。 这就是假阳性的代价:多一次线性扫描。
13.14.2 假阳性率的估算
- 一个节点通常只声明 1--5 个 provider,256 bit 里只占几个
- 但 cumulative bloom 是祖先的并集,越靠近根越"满"
如果一条链上有 50 个不同的 token,cumulative bloom 里约 50 个 bit 被置位(有重叠会更少)。此时假阳性率 ≈ 50/256 ≈ 20%。
这意味着:平均每 5 次向上查找,有 1 次是白跑的。但这仍然远好于"每层都线性扫描所有 provider"。
而且 Angular 不在乎这点浪费------因为绝大多数情况下,第一个节点就命中了(provider 就在自己或父节点上),根本不会走到长链。
13.14.3 两处 bloomHasToken 的分工
lookupTokenUsingNodeInjector 的循环里有两处 bloomHasToken,查的是不同数组:
ts
// ① 查自己:tView.data(node bloom)
if (bloomHasToken(bloomHash, injectorIndex, tView.data)) {
const instance = searchTokensOnInjector<T>(...);
if (instance !== NOT_FOUND) return instance;
}
parentLocation = lView[injectorIndex + NodeInjectorOffset.PARENT];
if (
parentLocation !== NO_PARENT_INJECTOR &&
shouldSearchParent(flags, lView[TVIEW].data[injectorIndex + NodeInjectorOffset.TNODE] === hostTElementNode) &&
// ② 查祖先:lView(cumulative bloom)
bloomHasToken(bloomHash, injectorIndex, lView)
) {
previousTView = tView;
injectorIndex = getParentInjectorIndex(parentLocation);
lView = getParentInjectorView(parentLocation, lView);
} else {
injectorIndex = -1; // 剪枝
}
① 用 tView.data:我这个节点自己有没有? ② 用 lView:我的祖先们有没有?(决定要不要继续爬)
这个分工如果不看清,会以为两处是重复代码。它们查的是两份不同的数据。
13.15 RelativeInjectorLocation:一个整数装下"跨视图父指针"
ts
export type RelativeInjectorLocation = number & {
__brand__: 'RelativeInjectorLocationFlags';
};
export const enum RelativeInjectorLocationFlags {
InjectorIndexMask = 0b111111111111111, // 低 15 位
ViewOffsetShift = 16,
NO_PARENT = -1,
}
export const NO_PARENT_INJECTOR = -1 as RelativeInjectorLocation;
13.15.1 位布局
arduino
31 16 15 0
┌───────────┬─────────────────┐
│ ViewOffset│ InjectorIndex │
│ (16 bit) │ (15 bit) │
└───────────┴─────────────────┘
- 低 15 位 :父注入器在它所在 LView 里的索引。15 位 = 最大 32767,足够(LView 索引通常几百)
- 第 16 位起 :要向上走几层
DECLARATION_VIEW才能到达那个 LView
InjectorIndexMask 只有 15 位(0b111111111111111),不是 16 位。为什么?
因为 NO_PARENT_INJECTOR = -1,而 -1 的二进制是全 1。如果 mask 是 16 位,-1 & 0xFFFF 仍然是 0xFFFF,无法区分。用 15 位则 -1 会被 assertNotEqual(parentLocation, -1) 显式拦截(见 getParentInjectorIndex 的 dev 断言)。
13.15.2 解码
ts
export function getParentInjectorIndex(parentLocation: RelativeInjectorLocation): number {
if (ngDevMode) {
assertNumber(parentLocation, 'Number expected');
assertNotEqual(parentLocation as any, -1, 'Not a valid state.');
const parentInjectorIndex = parentLocation & RelativeInjectorLocationFlags.InjectorIndexMask;
assertGreaterThan(parentInjectorIndex, HEADER_OFFSET,
'Parent injector must be pointing past HEADER_OFFSET.');
}
return parentLocation & RelativeInjectorLocationFlags.InjectorIndexMask;
}
export function getParentInjectorViewOffset(parentLocation: RelativeInjectorLocation): number {
return parentLocation >> RelativeInjectorLocationFlags.ViewOffsetShift;
}
export function getParentInjectorView(location: RelativeInjectorLocation, startView: LView): LView {
let viewOffset = getParentInjectorViewOffset(location);
let parentView = startView;
// <ng-template> tags or inline views, where the parent injector might live many views
// above the child injector.
while (viewOffset > 0) {
parentView = parentView[DECLARATION_VIEW]!;
viewOffset--;
}
return parentView;
}
getParentInjectorView 的循环注释解释了为什么不能只走一步:深层嵌套的 <ng-template>。
scss
<app-root> LView(root)
<my-cmp> LView(my-cmp) DECLARATION_VIEW = root
<ng-template> LView(tpl-1) DECLARATION_VIEW = my-cmp
<ng-template> LView(tpl-2) DECLARATION_VIEW = my-cmp(嵌在 tpl-1 里但声明在 my-cmp)
<div> ← 这里要找父注入器
tpl-2 的父注入器可能在 my-cmp 里(viewOffset = 1),也可能在 root 里(viewOffset = 2)。所以用循环。
13.15.3 getParentInjectorLocation:计算父位置
ts
export function getParentInjectorLocation(tNode: TNode, lView: LView): RelativeInjectorLocation {
if (tNode.parent && tNode.parent.injectorIndex !== -1) {
// 父 TNode 有注入器 → 就在当前 LView 里,ViewOffset = 0
return tNode.parent.injectorIndex as RelativeInjectorLocation;
}
let declarationViewOffset = 0;
let parentTNode: TNode|null = null;
let lViewCursor: LView|null = lView;
while (lViewCursor !== null) {
parentTNode = getTNodeFromLView(lViewCursor);
if (parentTNode === null) {
return NO_PARENT_INJECTOR;
}
ngDevMode && parentTNode && assertTNodeForLView(parentTNode!, lViewCursor[DECLARATION_VIEW]!);
declarationViewOffset++;
lViewCursor = lViewCursor[DECLARATION_VIEW];
if (parentTNode.injectorIndex !== -1) {
return (parentTNode.injectorIndex |
(declarationViewOffset << RelativeInjectorLocationFlags.ViewOffsetShift)) as RelativeInjectorLocation;
}
}
return NO_PARENT_INJECTOR;
}
两条路径:
快路径:父 TNode 就在同一个 LView 里且有 injectorIndex → 直接返回(ViewOffset = 0)
慢路径 :沿 DECLARATION_VIEW 向上,每上一层 declarationViewOffset++,直到找到有 injectorIndex 的宿主 TNode
注意 declarationViewOffset++ 在取 DECLARATION_VIEW 之前执行。这样第一次循环时 offset = 1,对应"上一层的宿主节点"。
13.16 lookupTokenUsingNodeInjector:完整推演
这是节点注入器的主循环(di.ts 506 行)。我把整个流程拆开:
ts
function lookupTokenUsingNodeInjector<T>(
tNode: TDirectiveHostNode, lView: LView, token: ProviderToken<T>,
flags: InternalInjectFlags, notFoundValue?: any,
) {
const bloomHash = bloomHashBitOrFactory(token);
if (typeof bloomHash === 'function') { /* 特殊 token,见 §13.13.2 */ }
else if (typeof bloomHash === 'number') {
let previousTView: TView|null = null;
let injectorIndex = getInjectorIndex(tNode, lView);
let parentLocation = NO_PARENT_INJECTOR;
let hostTElementNode: TNode|null =
flags & InternalInjectFlags.Host ? lView[DECLARATION_COMPONENT_VIEW][T_HOST] : null;
// ① 起点调整:SkipSelf 或本节点无注入器 → 从父开始
if (injectorIndex === -1 || flags & InternalInjectFlags.SkipSelf) {
parentLocation =
injectorIndex === -1
? getParentInjectorLocation(tNode, lView)
: lView[injectorIndex + NodeInjectorOffset.PARENT];
if (parentLocation === NO_PARENT_INJECTOR || !shouldSearchParent(flags, false)) {
injectorIndex = -1;
} else {
previousTView = lView[TVIEW];
injectorIndex = getParentInjectorIndex(parentLocation);
lView = getParentInjectorView(parentLocation, lView);
}
}
// ② 主循环
while (injectorIndex !== -1) {
ngDevMode && assertNodeInjector(lView, injectorIndex);
const tView = lView[TVIEW];
ngDevMode && assertTNodeForLView(tView.data[injectorIndex + NodeInjectorOffset.TNODE] as TNode, lView);
if (bloomHasToken(bloomHash, injectorIndex, tView.data)) {
const instance: T|{}|null = searchTokensOnInjector<T>(
injectorIndex, lView, token, previousTView, flags, hostTElementNode);
if (instance !== NOT_FOUND) return instance;
}
parentLocation = lView[injectorIndex + NodeInjectorOffset.PARENT];
if (
parentLocation !== NO_PARENT_INJECTOR &&
shouldSearchParent(flags, lView[TVIEW].data[injectorIndex + NodeInjectorOffset.TNODE] === hostTElementNode) &&
bloomHasToken(bloomHash, injectorIndex, lView)
) {
previousTView = tView;
injectorIndex = getParentInjectorIndex(parentLocation);
lView = getParentInjectorView(parentLocation, lView);
} else {
injectorIndex = -1;
}
}
}
return notFoundValue;
}
13.16.1 hostTElementNode 与 @Host
ts
let hostTElementNode: TNode|null =
flags & InternalInjectFlags.Host ? lView[DECLARATION_COMPONENT_VIEW][T_HOST] : null;
@Host 的语义是"最多查到宿主组件为止"。DECLARATION_COMPONENT_VIEW[T_HOST] 就是声明这个视图的那个组件的宿主 TNode。
然后在循环里:
ts
shouldSearchParent(
flags,
lView[TVIEW].data[injectorIndex + NodeInjectorOffset.TNODE] === hostTElementNode
)
一旦当前节点就是宿主 TNode,isFirstHostTNode = true,shouldSearchParent 返回 false(在 @Host 下)→ 循环终止。
ts
function shouldSearchParent(flags: InternalInjectFlags, isFirstHostTNode: boolean): boolean|number {
return !(flags & InternalInjectFlags.Self) && !(flags & InternalInjectFlags.Host && isFirstHostTNode);
}
@Host 的实现就是这一个布尔表达式:不是 Self,且(不是 Host 或还没到宿主)。
13.16.2 previousTView 的作用
previousTView 只在跨视图时被赋值:
ts
previousTView = tView; // 记录跨越前的 TView
injectorIndex = getParentInjectorIndex(parentLocation);
lView = getParentInjectorView(parentLocation, lView);
它的唯一用途是给 searchTokensOnInjector 判断"能不能看 viewProviders"(§13.17)。
这就是"只有从组件视图跨出去的那一刻才能看见 viewProviders"的实现 ------previousTView != currentTView 表示"刚刚跨了视图边界"。
13.16.3 完整推演:一个三层组件树
less
AppComponent (LView-A)
providers: [LoggerA]
└── <my-cmp> MyComponent (LView-B)
providers: [LoggerB]
viewProviders: [SecretB]
└── <div> ChildDirective (在 LView-B 里)
→ inject(LoggerA)
初始状态:
tNode= div 的 TNodelView= LView-Btoken= LoggerA,bloomHash= 例如 42(LoggerA 的 ID & 255)
步骤 1 :getInjectorIndex(divTNode, LView-B)
div 自己没有 providers,tNode.injectorIndex === -1 → 返回 -1
步骤 2 :进入 ①,getParentInjectorLocation(divTNode, LView-B)
divTNode.parent 是 my-cmp 的 TNode,它有 injectorIndex(因为 MyComponent 有 providers)→ 返回 myCmp.injectorIndex,ViewOffset = 0
步骤 3:
ts
previousTView = LView-B[TVIEW];
injectorIndex = myCmp.injectorIndex;
lView = LView-B; // ViewOffset = 0,不走 DECLARATION_VIEW
步骤 4:循环第一轮
bloomHasToken(42, myCmpIdx, tViewB.data)→ my-cmp 只提供 LoggerB/SecretB,bit 42 未置位 → false- 跳过
searchTokensOnInjector parentLocation = LView-B[myCmpIdx + 8]→ 指向 AppComponent 的注入器(ViewOffset = 1)shouldSearchParent(flags, false)→ truebloomHasToken(42, myCmpIdx, LView-B)→ 查 cumulative → AppComponent 提供了 LoggerA,bit 42 已置位 → truepreviousTView = tViewBinjectorIndex = appIdx,lView = LView-A(走一次DECLARATION_VIEW)
步骤 5:循环第二轮
bloomHasToken(42, appIdx, tViewA.data)→ AppComponent 的 node bloom 有 LoggerA → truesearchTokensOnInjector(appIdx, LView-A, LoggerA, previousTView=tViewB, ...)→ 找到!- 返回 LoggerA 实例
关键观察 :步骤 4 的 cumulative bloom 检查避免了"如果 AppComponent 也没有,还得往上爬"的浪费。而更重要的------如果 cumulative 说没有,这里就直接 injectorIndex = -1 退出了,根本不会去 LView-A。
13.17 searchTokensOnInjector:viewProviders 的可见性算法
ts
function searchTokensOnInjector<T>(
injectorIndex: number, lView: LView, token: ProviderToken<T>,
previousTView: TView|null, flags: InternalInjectFlags, hostTElementNode: TNode|null,
) {
const currentTView = lView[TVIEW];
const tNode = currentTView.data[injectorIndex + NodeInjectorOffset.TNODE] as TNode;
const canAccessViewProviders =
previousTView == null
? isComponentHost(tNode) && includeViewProviders
: previousTView != currentTView && (tNode.type & TNodeType.AnyRNode) !== 0;
const isHostSpecialCase = flags & InternalInjectFlags.Host && hostTElementNode === tNode;
const injectableIdx = locateDirectiveOrProvider(
tNode, currentTView, token, canAccessViewProviders, isHostSpecialCase);
if (injectableIdx !== null) {
return getNodeInjectable(lView, currentTView, injectableIdx, tNode as TElementNode, flags);
} else {
return NOT_FOUND;
}
}
13.17.1 canAccessViewProviders 的两个分支
源码注释给了完整的两个场景:
分支 A:previousTView == null(第一次调用,就在起点节点)
ts
isComponentHost(tNode) && includeViewProviders
只有两个条件都满足才可见:
isComponentHost(tNode):这个节点是组件的宿主includeViewProviders:全局开关(§13.8.3)
对应 §13.8.3 的例子:组件自己的构造函数里 includeViewProviders = true(初始值),所以能看到自己的 viewProviders。
分支 B:previousTView != null(正在向上爬)
ts
previousTView != currentTView && (tNode.type & TNodeType.AnyRNode) !== 0
previousTView != currentTView:刚刚跨过了视图边界tNode.type & TNodeType.AnyRNode:父节点是一个真实 DOM 节点(Element / Container / ElementContainer)
为什么第二个条件?因为跨视图边界后到达的 TNode 应该是组件的宿主元素(一个 Element)。如果到达的是 Text 节点或一个投影占位,语义上不对。
综合起来:
scss
MyComponent 的 view 内的某个节点 inject(SecretB)
→ 向上爬,跨出 MyComponent 的 view,到达 <my-cmp> 这个 Element TNode
→ previousTView(MyComponent的模板TView) != currentTView(父模板TView) ✓
→ tNode.type 是 Element ✓
→ canAccessViewProviders = true → 能看到 SecretB ✓
AppComponent 的 view 内的节点 inject(SecretB)
→ 向上爬,但根本没进过 MyComponent 的 view
→ previousTView == currentTView(都在 AppComponent 的模板里)✗
→ 看不到 SecretB ✓(正确!viewProviders 只在组件自己的视图里可见)
13.17.2 isHostSpecialCase
ts
const isHostSpecialCase = flags & InternalInjectFlags.Host && hostTElementNode === tNode;
@Host 且当前正好在宿主节点上时,行为特殊:只能看到 viewProviders 和组件本身,看不到其他指令的 providers。
这个限制传给 locateDirectiveOrProvider 的 endIndex 参数。
13.17.3 locateDirectiveOrProvider:三段区间扫描
ts
export function locateDirectiveOrProvider<T>(
tNode: TNode, tView: TView, token: ProviderToken<T>|string,
canAccessViewProviders: boolean, isHostSpecialCase: boolean|number,
): number|null {
const nodeProviderIndexes = tNode.providerIndexes;
const tInjectables = tView.data;
const injectablesStart = nodeProviderIndexes & TNodeProviderIndexes.ProvidersStartIndexMask;
const directivesStart = tNode.directiveStart;
const directiveEnd = tNode.directiveEnd;
const cptViewProvidersCount = nodeProviderIndexes >> TNodeProviderIndexes.CptViewProvidersCountShift;
const startingIndex = canAccessViewProviders
? injectablesStart
: injectablesStart + cptViewProvidersCount;
const endIndex = isHostSpecialCase ? injectablesStart + cptViewProvidersCount : directiveEnd;
for (let i = startingIndex; i < endIndex; i++) {
const providerTokenOrDef = tInjectables[i] as ProviderToken<any>|DirectiveDef<any>|string;
if (
(i < directivesStart && token === providerTokenOrDef) ||
(i >= directivesStart && (providerTokenOrDef as DirectiveDef<any>).type === token)
) {
return i;
}
}
if (isHostSpecialCase) {
const dirDef = tInjectables[directivesStart] as DirectiveDef<any>;
if (dirDef && isComponentDef(dirDef) && dirDef.type === token) {
return directivesStart;
}
}
return null;
}
tView.data 在这个节点上的布局是三段:
markdown
injectablesStart directiveEnd
│ │
├──── viewProviders ────┬──── providers ────┬─── directives ───┤
│ (cptViewProviders │ │ │
│ Count 个) │ │ │
directivesStart ─────────┘
- 段 1 :
viewProviders(cptViewProvidersCount个) - 段 2 :
providers - 段 3 :指令/组件定义本身(
directivesStart到directiveEnd)
比较方式不同:
- 段 1、2 存的是 token (
token === providerTokenOrDef,引用比较) - 段 3 存的是 DirectiveDef (
(def).type === token,要取.type)
canAccessViewProviders 决定 start,isHostSpecialCase 决定 end。
| 场景 | start | end |
|---|---|---|
| 普通查找,可看 VP | injectablesStart |
directiveEnd |
| 普通查找,不可看 VP | injectablesStart + cptViewProvidersCount |
directiveEnd |
@Host 在宿主节点 |
injectablesStart(若可看 VP) |
injectablesStart + cptViewProvidersCount |
注意第三种情况的 end 落在段 1 末尾 ------所以 @Host 在宿主节点上时,只看 viewProviders。
然后那个补充检查:
ts
if (isHostSpecialCase) {
const dirDef = tInjectables[directivesStart] as DirectiveDef<any>;
if (dirDef && isComponentDef(dirDef) && dirDef.type === token) {
return directivesStart;
}
}
因为组件定义总是排在 directives 段的第一位 (resolveProvider 的注释:"this method is called first for the component (if any), and then for other directives"),所以 @Host 特例要额外把组件本身加回来。
这就是 @Host 语义的完整实现:能看到 viewProviders + 组件自己,看不到同节点其他指令的 providers。
13.17.4 TNodeProviderIndexes 的位打包
ts
export const enum TNodeProviderIndexes {
/** The index of the first provider on this node is encoded on the least significant bits. */
ProvidersStartIndexMask = 0b00000000000011111111111111111111, // 低 20 位
/** The count of view providers from the component on this node is encoded on the 20 most significant bits. */
CptViewProvidersCountShift = 20,
CptViewProvidersCountShifter = 0b00000000000100000000000000000000,
}
一个 32 位整数装两个数:
- 低 20 位:本节点第一个 provider 在
tView.data里的索引(最大 1M) - 高 12 位:viewProviders 的个数(最大 4095)
(注释说 "20 most significant bits",实际是 12 位------源码注释与代码不一致,这是 v17 重构后遗留的文案错误。以代码为准。)
每次注册一个 viewProvider:
ts
if (isViewProvider) {
tNode.providerIndexes += TNodeProviderIndexes.CptViewProvidersCountShifter;
}
CptViewProvidersCountShifter = 1 << 20,所以 += 等价于高 12 位加 1。
13.18 getNodeInjectable:NodeInjectorFactory 与实例化
找到索引后,取真实值:
ts
export function getNodeInjectable(
lView: LView, tView: TView, index: number, tNode: TDirectiveHostNode, flags?: InternalInjectFlags,
): any {
let value = lView[index];
const tData = tView.data;
if (value instanceof NodeInjectorFactory) {
const factory: NodeInjectorFactory = value;
ngDevMode && injectionPath.push(factory.name ?? 'unknown');
if (factory.resolving) {
let token = '';
if (ngDevMode) {
token = stringifyForError(tData[index]);
throw cyclicDependencyErrorWithDetails(token, injectionPath);
} else {
throw cyclicDependencyError(token);
}
}
const previousIncludeViewProviders = setIncludeViewProviders(factory.canSeeViewProviders);
factory.resolving = true;
const token = (tData[index] as DirectiveDef<unknown>|ComponentDef<unknown>).type || tData[index];
let prevInjectContext: InjectorProfilerContext|undefined;
if (ngDevMode) {
const injector = new NodeInjector(tNode, lView);
prevInjectContext = setInjectorProfilerContext({injector, token});
}
const previousInjectImplementation = factory.injectImpl
? setInjectImplementation(factory.injectImpl)
: null;
const success = enterDI(lView, tNode, InternalInjectFlags.Default);
ngDevMode && assertEqual(success, true,
"Because flags do not contain `SkipSelf' we expect this to always succeed.");
try {
ngDevMode && emitInjectorToCreateInstanceEvent(token);
value = lView[index] = factory.factory(undefined, flags, tData, lView, tNode);
ngDevMode && emitInstanceCreatedByInjectorEvent(value);
// 只有指令才注册 pre-order hooks,provider 跳过
if (tView.firstCreatePass && index >= tNode.directiveStart) {
ngDevMode && assertDirectiveDef(tData[index]);
registerPreOrderHooks(index, tData[index] as DirectiveDef<any>, tView);
}
} finally {
ngDevMode && setInjectorProfilerContext(prevInjectContext!);
previousInjectImplementation !== null && setInjectImplementation(previousInjectImplementation);
setIncludeViewProviders(previousIncludeViewProviders);
factory.resolving = false;
leaveDI();
ngDevMode && (injectionPath = []);
}
}
return value;
}
13.18.1 lView[index] 的双重身份
lView[index] 可能是:
- 一个
NodeInjectorFactory(还没实例化) - 真实值(已实例化)
这是因为 resolveProvider 把 factory 推进了 lView 和 blueprint(§13.11.4)。所以每个 LView 在创建时就带着所有 provider 的 factory。
这带来一个内存开销:每个节点注入器的实例,都要为它的每个 provider 在 LView 里占一个槽位。但这是必要的------provider 的值是每实例 的(组件 A 的 LoggerA 和组件 B 的 LoggerA 是两个实例)。
13.18.2 factory.resolving:循环依赖检测(第二套)
与环境注入器的 CIRCULAR 哨兵不同,节点注入器用 NodeInjectorFactory.resolving 布尔标志。
两者语义相同,但实现方式不同:
R3Injector用value = CIRCULAR(因为 Record 是{factory, value, multi}结构)NodeInjector用factory.resolving(因为 factory 对象本身就是状态载体)
injectionPath 是 dev 模式下的额外投资:记录路径 ['A', 'B', 'A'],让错误信息能显示完整循环链。
13.18.3 NodeInjectorFactory 的三个隐藏类
源码注释:
markdown
* NOTE: Some of the fields are optional which means that this class has two hidden classes.
* - One without `multi` support (most common)
* - One with `multi` values, (rare).
*
* Since VMs can cache up to 4 inline hidden classes this is OK.
*
* - Single factory: Only `resolving` and `factory` is defined.
* - `providers` factory: `componentProviders` is a number and `index = -1`.
* - `viewProviders` factory: `componentProviders` is a number and `index` points to `providers`.
注释说"两个隐藏类",但下面列了三种形态------注释本身也有点过时。核心思想是:V8 能缓存 4 个内联隐藏类,所以 3 种形态不会导致字典模式降级。
字段清单:
ts
export class NodeInjectorFactory {
injectImpl: null | (<T>(token: ProviderToken<T>, flags?: InternalInjectFlags) => T);
resolving = false;
canSeeViewProviders: boolean;
multi?: Array<() => any>;
componentProviders?: number;
index?: number;
providerFactory?: NodeInjectorFactory | null;
constructor(
public factory: (this: NodeInjectorFactory, _: undefined, flags: InternalInjectFlags|undefined,
tData: TData, lView: LView, tNode: TDirectiveHostNode) => any,
isViewProvider: boolean,
injectImplementation: null | (<T>(token: ProviderToken<T>, flags?: InternalInjectFlags) => T),
public name: string | null,
) {
ngDevMode && assertDefined(factory, 'Factory not specified');
ngDevMode && assertEqual(typeof factory, 'function', 'Expected factory function.');
this.canSeeViewProviders = isViewProvider;
this.injectImpl = injectImplementation;
}
}
13.18.4 registerPreOrderHooks 的条件
ts
if (tView.firstCreatePass && index >= tNode.directiveStart) {
ngDevMode && assertDirectiveDef(tData[index]);
registerPreOrderHooks(index, tData[index] as DirectiveDef<any>, tView);
}
源码注释解释了为什么加 index >= tNode.directiveStart:
arduino
// This code path is hit for both directives and providers.
provider 也可能有 ngOnInit (如果你 {provide: X, useClass: Y} 且 Y 有 ngOnInit)。但 Angular 只在 directives 段注册 pre-order hooks。
那么 provider 的 ngOnInit 怎么办?------不调用。
这是一个隐含的语义:只有指令和组件的生命周期钩子会被框架调用,providers 里声明的普通类的钩子不会。
(但 ngOnDestroy 会!见 §13.19.3。)
13.19 providersResolver:providers 与 viewProviders 的插入顺序
packages/core/src/render3/di_setup.ts(431 行)。
13.19.1 处理顺序的承诺
ts
/**
* Resolves the providers which are defined in the DirectiveDef.
*
* When inserting the tokens and the factories in their respective arrays, we can assume that
* this method is called first for the component (if any), and then for other directives on the same
* node.
* As a consequence,the providers are always processed in that order:
* 1) The view providers of the component
* 2) The providers of the component
* 3) The providers of the other directives
* This matches the structure of the injectables arrays of a view (for each node).
*/
这个顺序决定了 tView.data 里三段区间的形成:
scss
resolveProvider(viewProviders, ...) → 段 1
resolveProvider(providers, ...) → 段 2(组件)
resolveProvider(providers, ...) → 段 2(其他指令)
[指令定义] → 段 3
调用点在 render3/view/directives.ts:
ts
// 先:注册指令自己到 bloom
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, lView), tView, def.type);
...
// 后:解析 providers
if (def.providersResolver) {
def.providersResolver(def);
}
13.19.2 非 multi provider 的处理
ts
const factory = new NodeInjectorFactory(
providerFactory, isViewProvider, ɵɵdirectiveInject, ngDevMode ? providerName(provider) : null,
);
const existingFactoryIndex = indexOf(
token, tInjectables,
isViewProvider ? beginIndex : beginIndex + cptViewProvidersCount,
endIndex,
);
if (existingFactoryIndex === -1) {
diPublicInInjector(getOrCreateNodeInjectorForNode(...), tView, token);
registerDestroyHooksIfSupported(tView, provider, tInjectables.length);
tInjectables.push(token);
tNode.directiveStart++;
tNode.directiveEnd++;
if (isViewProvider) {
tNode.providerIndexes += TNodeProviderIndexes.CptViewProvidersCountShifter;
}
lInjectablesBlueprint.push(factory);
lView.push(factory);
} else {
lInjectablesBlueprint[existingFactoryIndex] = factory;
lView[existingFactoryIndex] = factory;
}
tNode.directiveStart++ 这一行是最反直觉的。
为什么注册一个 provider 要让 directiveStart 加 1?
因为 tView.data 的布局是 [...providers..., ...directives...],而 directiveStart 是后段的起始位置 。新 provider 被 push 到数组末尾(当前 directiveStart 的位置),所以要把它往后推一格。
换句话说:directiveStart 同时是"providers 段的结束"和"directives 段的开始"。 它在这个数组里是一个"游标"。
而 indexOf 的查找范围是 [beginIndex, endIndex) = [injectablesStart, directiveStart)------正好是 providers 段。
重复 provider 的覆盖 :如果 token 已存在(existingFactoryIndex !== -1),不新增 token,只替换 factory。这就是"后声明的 provider 覆盖先声明的"。
注意 indexOf 的起始位置对 viewProvider 和 provider 不同:
isViewProvider→ 从beginIndex开始(段 1)- 否则 → 从
beginIndex + cptViewProvidersCount开始(段 2)
所以 viewProviders 和 providers 可以声明同一个 token,且互不覆盖------这正是 §13.8.3 那个例子能工作的原因。
13.19.3 registerDestroyHooksIfSupported
ts
function registerDestroyHooksIfSupported(
tView: TView, provider: Exclude<Provider, any[]>, contextIndex: number, indexInFactory?: number,
) {
const providerIsTypeProvider = isTypeProvider(provider);
const providerIsClassProvider = isClassProvider(provider);
if (providerIsTypeProvider || providerIsClassProvider) {
const classToken = providerIsClassProvider ? resolveForwardRef(provider.useClass) : provider;
const prototype = classToken.prototype;
const ngOnDestroy = prototype.ngOnDestroy;
if (ngOnDestroy) {
const hooks = tView.destroyHooks || (tView.destroyHooks = []);
if (!providerIsTypeProvider && (provider as ClassProvider).multi) {
const existingCallbacksIndex = hooks.indexOf(contextIndex);
if (existingCallbacksIndex === -1) {
hooks.push(contextIndex, [indexInFactory, ngOnDestroy]);
} else {
(hooks[existingCallbacksIndex + 1] as DestroyHookData).push(indexInFactory!, ngOnDestroy);
}
} else {
hooks.push(contextIndex, ngOnDestroy);
}
}
}
}
只有 TypeProvider 和 ClassProvider 支持 ngOnDestroy ------useValue / useFactory / useExisting 都不支持。因为它们没有 prototype 可查。
destroyHooks 的布局(第 12 章讲过)是 [index, hook] 或 [index, [[i, hook], [i, hook]]](multi)。
注意这段只查 prototype,不查实例。所以:
ts
class Foo {
ngOnDestroy = () => {}; // ← 实例字段,查不到!
}
用箭头函数做实例字段是不会被调用的。必须写成方法:
ts
class Foo {
ngOnDestroy() {} // ← prototype 上有,能查到
}
这是一个很常见的坑。
13.19.4 multi provider:双工厂链接
这是 di_setup.ts 里最复杂的部分。源码给了 20 行注释:
vbnet
* The algorithm to process multi providers is as follows:
* 1) If the multi provider comes from the `viewProviders` of the component:
* a) If the special view providers factory doesn't exist, it is created and pushed.
* b) Else, the multi provider is added to the existing multi factory.
* 2) If the multi provider comes from the `providers` of the component or of another directive:
* a) If the multi factory doesn't exist, it is created and provider pushed into it.
* It is also linked to the multi factory for view providers, if it exists.
* b) Else, the multi provider is added to the existing multi factory.
关键在于两个工厂对象:
css
tView.data 布局(某节点的 providers 段):
┌──────────────────────┬──────────────────────┬─────────────┐
│ viewProviders 段 │ providers 段 │ directives │
│ [multiVPFactory] │ [multiPFactory] │ │
│ ↑ │ ↑ │ │
│ │ providerFactory│──────┘ │ │
│ └────────────────┘ (VP factory 持有 P factory 的引用) │
└──────────────────────┴──────────────────────┴─────────────┘
ts
if (!isViewProvider && doesViewProvidersFactoryExist) {
lInjectablesBlueprint[existingViewProvidersFactoryIndex].providerFactory = factory;
}
然后 multiViewProvidersFactoryResolver:
ts
function multiViewProvidersFactoryResolver(
this: NodeInjectorFactory, _: undefined, _flags: InternalInjectFlags|undefined,
_tData: TData, lView: LView, tNode: TDirectiveHostNode,
): any[] {
const factories = this.multi!;
let result: any[];
if (this.providerFactory) {
const componentCount = this.providerFactory.componentProviders!;
const multiProviders = getNodeInjectable(lView, lView[TVIEW], this.providerFactory!.index!, tNode);
result = multiProviders.slice(0, componentCount);
// Insert the `viewProvider` instances.
multiResolve(factories, result);
for (let i = componentCount; i < multiProviders.length; i++) {
result.push(multiProviders[i]);
}
} else {
result = [];
multiResolve(factories, result);
}
return result;
}
componentProviders 计数器 的作用是保证顺序。源码注释的例子:
ts
component:
providers: [{provide: String, useValue: 'component', multi: true}],
viewProviders:[{provide: String, useValue: 'componentView', multi: true}],
directive:
providers: [{provide: String, useValue: 'directive', multi: true}],
期望结果:
- content 注入(看不到 VP):
['component', 'directive'] - view 注入(看得到 VP):
['component', 'componentView', 'directive']
viewProviders 被插在"组件的 providers"之后、"其他指令的 providers"之前。
componentProviders 记录"组件贡献了几个 multi 项"(这里是 1),于是:
multiProviders.slice(0, 1)=['component']- 插入 VP:
['component', 'componentView'] - 追加剩余:
['component', 'componentView', 'directive']✓
而且源码注释特别强调:
We further have to make sure that the shared instances (in our case
all) are the exact same instance in both the content as well as the view injection.
因为 VP factory 是通过 getNodeInjectable 去拿 P factory 的结果 (不是重新执行),所以 'component' 这个实例在两种注入里是同一个对象。
13.20 四种修饰标志的位实现
InternalInjectFlags 是一个位标志枚举:
ts
export enum InternalInjectFlags {
Default = 0b00000000,
Self = 0b00000010,
SkipSelf = 0b00000100,
Host = 0b00001000,
Optional = 0b00010000,
// ...
}
(实际定义在 di/interface/injector.ts)
13.20.1 @Self
ts
// lookupTokenUsingNodeInjector, ① 起点调整
if (injectorIndex === -1 || flags & InternalInjectFlags.SkipSelf) { ... }
// ② 循环里
shouldSearchParent(flags, ...) // 返回 !(flags & Self) && ...
Self 让 shouldSearchParent 恒为 false → 只查起点节点。
13.20.2 @SkipSelf
ts
if (injectorIndex === -1 || flags & InternalInjectFlags.SkipSelf) {
parentLocation = injectorIndex === -1
? getParentInjectorLocation(tNode, lView)
: lView[injectorIndex + NodeInjectorOffset.PARENT];
...
}
SkipSelf 强制起点跳到父节点。
13.20.3 @Host
见 §13.16.1。
13.20.4 @Optional
ts
notFoundValue =
flags & InternalInjectFlags.Optional && notFoundValue === THROW_IF_NOT_FOUND
? null
: notFoundValue;
return nextInjector.get(token, notFoundValue);
Optional 只做一件事:把 THROW_IF_NOT_FOUND 换成 null 。然后 NullInjector 看到不是 THROW_IF_NOT_FOUND 就返回它。
这是一个漂亮的"用默认值表达控制流"的技巧 ------不需要额外的布尔参数,靠 notFoundValue 的身份就能传递"找不到时怎么办"。
13.20.5 组合语义
@Self() @Optional():
ts
const nextInjector = !(flags & InternalInjectFlags.Self) ? this.parent : getNullInjector();
Self 让下一跳直接是 NullInjector,Optional 让 notFoundValue = null → 返回 null。
@SkipSelf() @Host():SkipSelf 跳过起点,Host 在到达宿主时停止。
13.21 R3Injector.get 的错误路径:依赖路径的组装
ts
} catch (error: any) {
const errorCode = getRuntimeErrorCode(error);
if (
errorCode === RuntimeErrorCode.CYCLIC_DI_DEPENDENCY ||
errorCode === RuntimeErrorCode.PROVIDER_NOT_FOUND
) {
// Note: we use `if (ngDevMode) { ... }` instead of an early return.
// in production builds.
if (ngDevMode) {
prependTokenToDependencyPath(error, token);
if (previousInjector) {
throw error;
} else {
throw augmentRuntimeError(error, this.source);
}
} else {
throw new RuntimeError(errorCode, null);
}
} else {
throw error;
}
}
13.21.1 路径是"由内向外"组装的
错误在最深层抛出,每向上一层 prependTokenToDependencyPath(error, token) 把当前 token 加到路径前面。
scss
root.get(A)
→ A 的 factory → inject(B)
→ B 的 factory → inject(C)
→ C 找不到 → 抛 error(path=[])
→ catch: prepend(C) → path=[C]
→ catch: prepend(B) → path=[B, C]
→ catch: prepend(A) → path=[A, B, C]
最后在最外层(previousInjector 为 falsy,即没有更外层的注入器了)调 augmentRuntimeError(error, this.source) 格式化完整消息。
13.21.2 那段关于 esbuild 的注释
arduino
// inside a function body, so the block may remain in the bundle.
// in production builds.
这是一个构建工具行为的实战经验:
ts
// 不好的写法
if (!ngDevMode) { throw new RuntimeError(...); } // 后面还有 return
return;
// ... dev-only 代码
// 好的写法
if (ngDevMode) { /* dev-only 代码 */ }
esbuild 在 return 之后的代码删除上比较保守。写成 if (ngDevMode) {...} 则能被可靠识别。
这类注释是全仓库最有价值的知识之一------它不是讲算法,而是讲"我们踩过什么坑"。
13.22 destroy:销毁顺序与重入保护
ts
override destroy(): void {
assertNotDestroyed(this);
this._destroyed = true;
const prevConsumer = setActiveConsumer(null);
try {
for (const service of this._ngOnDestroyHooks) {
service.ngOnDestroy();
}
const onDestroyHooks = this._onDestroyHooks;
// themselves from mutating the array during iteration.
this._onDestroyHooks = [];
for (const hook of onDestroyHooks) {
hook();
}
} finally {
this.records.clear();
this._ngOnDestroyHooks.clear();
this.injectorDefTypes.clear();
setActiveConsumer(prevConsumer);
}
}
三处与第 12 章讲的视图销毁同构的设计:
- 先设
_destroyed = true再跑钩子 ------ 防止钩子重入destroy()(第 12 章:"onDestroy 其实是 afterDestroy") - 先重置数组再遍历 ------ 防止钩子 unregister 修改数组
setActiveConsumer(null)------ 销毁不是响应式的
finally 里 records.clear() 保证即使某个钩子抛异常,引用也会释放(防内存泄漏)。
13.23 实战:诊断与调试工具
13.23.1 导出注入器的 provider 清单
ts
// dev-only:利用 R3Injector.toString() 的 dev 实现
function dumpInjector(injector: Injector): string[] {
const anyInjector = injector as any;
if (!anyInjector.records) return ['(not an R3Injector)'];
return [...anyInjector.records.keys()].map((t: any) => {
const name = typeof t === 'function' ? t.name
: t?.ngMetadataName === 'InjectionToken' ? String(t)
: String(t);
const rec = anyInjector.records.get(t);
const state = rec === null ? 'NEGATIVE-CACHE'
: rec.value === (Object.getPrototypeOf(rec.value) === null ? rec.value : rec.value)
? (rec.value && (rec.value as any)?.__isNotYet ? 'NOT_YET' : 'value')
: '?';
return `${name} → ${state}`;
});
}
(注:R3Injector.toString() 在 ngDevMode 下已经做了类似的事,返回 R3Injector[Foo, Bar, ...]。)
13.23.2 诊断 bloom filter
ts
// 在浏览器控制台里检查某节点的 bloom filter 内容
function dumpBloom(lView: any, injectorIndex: number) {
const nodeBloom: number[] = [];
const cumBloom: number[] = [];
for (let i = 0; i < 8; i++) {
nodeBloom.push(lView[TVIEW].data[injectorIndex + i]);
cumBloom.push(lView[injectorIndex + i]);
}
console.log('node bloom (自己的 token):', nodeBloom.map(n => n.toString(2).padStart(32, '0')));
console.log('cum. bloom (祖先的 token):', cumBloom.map(n => n.toString(2).padStart(32, '0')));
console.log('parent loc:', lView[injectorIndex + 8]);
}
把 TVIEW 换成实际索引(第 09 章的槽位表:TVIEW = 1)。
13.23.3 查看 token 的 DI ID
ts
function ngElementId(token: any): number | undefined {
return Object.hasOwn(token, '__NG_ELEMENT_ID__') ? token.__NG_ELEMENT_ID__ : undefined;
}
// 例
console.log(ngElementId(MyService)); // 0(第一次 bloomAdd 时分配)
console.log(ngElementId(Injector)); // -1(InjectorMarkers.Injector)
如果你发现一个 service 的 __NG_ELEMENT_ID__ 是 undefined,说明它从未被任何节点注入器提供过------注入它会直接跳到环境注入器(§13.13.1)。
13.23.4 用 InjectorProfiler 追踪
Angular 22 有内建的注入器剖析器(dev 模式):
ts
import {ɵgetInjectorProfilerContext, ɵsetInjectorProfiler} from '@angular/core';
// 在 dev 模式下可用
(具体 API 以 goldens/public-api/core/index.api.md 为准。)
13.24 性能特性总结
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 环境注入器查找命中 | O(1) | Map.get |
| 环境注入器查找未命中 | O(depth) | 沿 parent 链,通常 ≤ 3 |
| token 从未在节点注入器出现 | O(0) | bloomHash === undefined 短路 |
| 节点注入器查找(命中起点) | O(1) + O(p) | 一次位运算 + 线性扫本节点 p 个 provider |
| 节点注入器查找(爬 d 层) | O(d) 位运算 + 少量线性扫描 | cumulative bloom 剪枝 |
| 节点注入器注册 provider | O(p) | indexOf 线性查重,只在 firstCreatePass |
| 内存(每个节点注入器) | 9 槽位 × 2 数组 | 零额外对象 |
关键结论:
- root service 的注入成本与组件树深度无关 (
undefined短路) - 节点 provider 的查找成本与"祖先链上有多少 provider"弱相关(bloom 剪枝)
- 节点注入器的内存开销是常数级的 ------9 个数字槽位,不需要
Map
13.31 全章贯通:一次 inject(Token) 走了哪条路
本章讲依赖注入的下半层:环境注入器与节点注入器两套机制。这一节用"一次 inject(Token) 到底走了哪条路"把 13.1 到 13.22 串起来。
为什么是两套(13.1)。 Angular 的 DI 要同时满足两件互相冲突的事:应用级单例(全局配置、服务)与组件级作用域(每个组件实例一份)。环境注入器 用 Map 精确查找,适合前者;节点注入器 用 bloom filter 做"可能命中"的快速判断,适合后者------因为节点注入器的查找发生在渲染热路径上,每次解析都做 Map 查找太贵了。
环境注入器:records Map 与三态值机(13.3、13.4)。 R3Injector 用一张 Map 存 provider,每个 record 有三态:未实例化 / 正在实例化 / 已实例化。hydrate 时才真正调用工厂------这就是惰性实例化。三态里的"正在实例化"状态是循环依赖检测的关键:第二次进入同一个 record 就说明成环了(13.4)。
五种 provider 归一为一个函数(13.5)。 useClass / useValue / useFactory / useExisting / 类本身,最后都被 providerToFactory 变成一个 () => any。运行时只有一种 provider,这个归一化让查找路径极简。
树摇(13.7)。 providedIn: 'root' 的服务不在任何 NgModule 里声明,而是在类的 ɵprov 静态字段上自带工厂。injectableDefInScope 判断这个服务是否"在作用域内",不在就不实例化、也不进包。这是 standalone 时代服务能被摇掉的唯一机制。
inject() 怎么知道自己能被调用(13.8、13.9)。 注入上下文由一个模块级变量维护:setCurrentInjector 进、runInInjectionContext 包。这也是为什么在 setTimeout 里直接调 inject() 会报错------上下文已经出了栈 。v22 新增的 injectAsync(13.10)则允许工厂返回 Promise。
节点注入器的内存布局(13.11)。 每个元素在 LView 里占 9 个槽位,其中若干槽位就是 bloom filter 的三个整数(BLOOM_SIZE=256,13.12)。这是"把 DI 信息塞进 LView"的典型例子------又一次印证第 9 章那套数组布局是万物的地基。
bloom filter:先问"可能吗",再问"真的吗"(13.12--13.14)。 每个 provider 的 token 被哈希成若干位,写进它所在注入器的 bloom 整数。查找时先做位与运算:位不命中就一定没有,直接跳到父注入器;位命中才去做精确的 Map 查找。假阳性(13.14)是允许的代价------它只会多一次查找,不会出错。
一个整数装下"跨视图父指针"(13.15)。 RelativeInjectorLocation 把"父注入器在哪个视图、偏移多少"编码进单个整数,这样向上遍历父链时不需要额外分配对象。
完整查找(13.16、13.17)。 lookupTokenUsingNodeInjector 沿父链向上,每一层先跑 bloom,命中就进 searchTokensOnInjector 精确查找;这一层还要处理 viewProviders 的可见性------viewProviders 只对当前视图的模板可见,对投影进来的内容不可见 (13.17)。这一条是 providers 与 viewProviders 区别的完整解释。
实例化与销毁(13.18、13.19、13.22)。 找到 factory 后由 getNodeInjectable 调用并缓存;providersResolver 负责在创建期把 providers / viewProviders 按正确顺序插入------顺序很重要,因为它决定了同 token 多 provider 时谁覆盖谁。销毁时逆序执行且带重入保护。
出错时(13.21)。 R3Injector.get 的错误路径会把"依赖路径"逐层拼出来,这就是为什么 Angular 的 DI 报错能显示完整的 A → B → C 链------这份可读性是有意为之的,代价是错误路径上要额外维护一个数组。
13.25 常见坑清单
13.25.1 viewProviders 的三条规则
- 只有组件能声明 (指令的
viewProviders会被忽略/报错) - 只对组件自己视图内的内容可见,投影进来的内容看不到
- 声明在
providers里的服务,看不见viewProviders(§13.8.3)
13.25.2 重复 provider 的覆盖顺序
ts
@Component({providers: [A, B, A]})
// 最后一个 A 生效
indexOf 找到第一个,lInjectablesBlueprint[idx] = factory 替换 factory。token 只占一个槽位,factory 被覆盖。
13.25.3 ngOnDestroy 必须是 prototype 方法
见 §13.19.3。用箭头函数实例字段会静默失效。
13.25.4 useFactory 里不能 inject
ts
// ✅ 正确
{provide: X, useFactory: () => inject(Y).makeX()}
// ❌ 错误:await 之后
{provide: X, useFactory: async () => { await something(); return inject(Y); }}
inject 必须在同步栈内。异步场景用 injectAsync(§13.10)或先抓 Injector。
13.25.5 循环依赖不一定报错
ts
@Injectable() class A { constructor(b: B) {} }
@Injectable() class B { constructor(a: A) {} }
会报 CyclicDependencyError。但:
ts
@Injectable() class A {
constructor(private injector: Injector) {}
getB() { return this.injector.get(B); }
}
不报错(延迟到调用时才解析)。这是合法的破环手段,但会掩盖设计问题。
13.25.6 inject(Injector) 的性能
每次都 new NodeInjector(...)(§13.13.2)。热路径上应该缓存:
ts
class Cmp {
private injector = inject(Injector); // 字段初始化器只跑一次
onClick() { this.injector.get(X); }
}
13.26 横向对比
| Angular | React Context | Vue provide/inject | NestJS | |
|---|---|---|---|---|
| 作用域单位 | 组件树节点 + 环境链 | 组件树(Provider 组件) | 组件实例链 | 模块 |
| 查找算法 | bloom filter 剪枝 + Map | 沿 Fiber 树线性向上 | 沿 parent 链线性向上 |
Map + 递归 |
| 惰性实例化 | ✅(NOT_YET 三态) |
❌(Provider 渲染时求值) | ❌(provide 时求值) | ✅ |
| 循环依赖检测 | ✅(两处) | ❌ | ❌ | ⚠️ 有限 |
| 类型安全 | ✅(token 即类型) | ⚠️(泛型擦除) | ⚠️(string key 或 Symbol) | ✅ |
| 树摇 | ✅(providedIn) |
❌(模块顶层 import) | ❌ | N/A |
| 多级覆盖 | ✅(Self/SkipSelf/Host) |
❌(最近 Provider 胜) | ❌ | ✅ |
Angular 独有三项 :bloom filter 剪枝、四种修饰符、编译期树摇的 providedIn。
13.27 完整手工演算:从 @Component 到 bloom 位
这一节把前面所有机制串起来,做一次逐位、逐槽位的推演。给一个真实(但极小)的场景:
ts
@Injectable({providedIn: 'root'})
class ApiService { constructor(private http: HttpClient) {} }
@Injectable()
class CardTheme { constructor(public color: string) {} }
@Injectable()
class CardLogger { log(m: string) {} }
@Component({
selector: 'app-root',
providers: [CardLogger, {provide: CardTheme, useValue: new CardTheme('dark')}],
template: `<my-card><span>hi</span></my-card>`,
})
class AppComponent {
api = inject(ApiService);
}
@Component({
selector: 'my-card',
providers: [CardLogger],
viewProviders: [{provide: CardTheme, useValue: new CardTheme('light')}],
template: `<ng-content></ng-content>`,
})
class MyCard {
theme = inject(CardTheme);
}
13.27.1 第一步:ID 分配
假设应用启动时 nextNgElementId = 0,按 bloomAdd 的调用顺序分配:
| token | 注册时机 | __NG_ELEMENT_ID__ |
bloomHash(& 255) |
桶(>> 5) |
掩码(1 << h) |
|---|---|---|---|---|---|
AppComponent |
指令注册(directives.ts:158) |
0 | 0 | 0 | 1 << 0 |
CardLogger(app-root) |
resolveProvider |
1 | 1 | 0 | 1 << 1 |
CardTheme(app-root) |
resolveProvider |
2 | 2 | 0 | 1 << 2 |
MyCard |
指令注册 | 3 | 3 | 0 | 1 << 3 |
CardLogger(my-card) |
resolveProvider |
4 | 4 | 0 | 1 << 4 |
CardTheme(my-card, VP) |
resolveProvider |
5 | 5 | 0 | 1 << 5 |
ApiService 没有 ID ------它是 providedIn: 'root',从没进过任何 providers 数组,所以从没被 bloomAdd。
13.27.2 第二步:tView.data 的槽位布局
app-root 的 TView.data(简化,只看 DI 相关):
yaml
索引 0..7 : node bloom(app-root 节点的 8 个桶)
索引 8 : app-root 的 TNode
索引 9..17 : my-card 节点的 bloom + TNode
索引 18 : CardLogger ← injectablesStart ≈ 18
索引 19 : CardTheme
索引 20.. : AppComponent 的 DirectiveDef ← directiveStart
(实际索引要加 HEADER_OFFSET 和 decls,这里只示意相对关系。)
app-root 节点的 node bloom(桶 0)=
scss
(1<<0) | (1<<1) | (1<<2)
= 0b0000_0000_0000_0000_0000_0000_0000_0111
即 bit 0(AppComponent)、bit 1(CardLogger)、bit 2(CardTheme)被置位。
my-card 节点的 node bloom(桶 0)=
scss
(1<<3) | (1<<4) | (1<<5)
= 0b0000_0000_0000_0000_0000_0000_0011_1000
13.27.3 第三步:cumulative bloom 的计算
ini
my-card 的 cumulative = my-card.parent(app-root) 的 cumulative | app-root 的 node bloom
= (app-root 自己的 cumulative) | 0b0111
而 app-root 是根组件,它的 parentLoc = NO_PARENT_INJECTOR(getParentInjectorLocation 走到底),所以 cumulative 段保持全 0 (insertBloom 推的 8 个 0)。
于是:
| 节点 | node bloom(桶0) | cumulative bloom(桶0) |
|---|---|---|
| app-root | 0b0111 |
0b0000 |
| my-card | 0b111000 |
0b0111 |
13.27.4 第四步:MyCard.theme = inject(CardTheme) 的查找过程
MyCard 的构造函数在 MyCard 的 LView 里执行,tNode = <my-card> 的 TNode,lView = LView-B(MyCard 自己的视图)。
等等------这里有个关键问题 :inject(CardTheme) 是从哪个节点开始找的?
答案:从声明这个组件的宿主节点开始 ,不是从组件内部。因为 ɵɵdirectiveInject 用的是 getCurrentTNode(),而在组件工厂执行时,当前 TNode 是 <my-card> 这个元素节点。
所以 tNode = my-card 的 TNode,lView = AppComponent 的 LView (LView-A,因为 <my-card> 声明在 AppComponent 的模板里)。
步骤 1 :bloomHashBitOrFactory(CardTheme) → 5 & 255 = 5(数字)
步骤 2 :getInjectorIndex(myCardTNode, LView-A) → my-card 有 providers,返回其 injectorIndex(= 9,示意)
步骤 3 :不是 SkipSelf,跳过起点调整。previousTView = null
步骤 4 :循环第一轮(injectorIndex = 9, lView = LView-A)
bloomHasToken(5, 9, tViewA.data):桶 0 的值是0b111000,掩码1 << 5 = 0b100000。0b111000 & 0b100000 = 0b100000→ true ✓searchTokensOnInjector(9, LView-A, CardTheme, previousTView=null, flags, null)tNode = tViewA.data[9 + 8]= my-card 的 TNodecanAccessViewProviders:previousTView == null→ 走分支 A:isComponentHost(tNode) && includeViewProvidersisComponentHost✓(my-card 是 MyCard 的宿主)includeViewProviders:这是组件自己的构造函数,初始值是true✓- →
canAccessViewProviders = true
locateDirectiveOrProvider(tNode, tViewA, CardTheme, true, false)injectablesStart= 18(示意,实际是 my-card 节点上的)cptViewProvidersCount= 1(只有 CardTheme 一个 VP)startingIndex = 18(可看 VP)endIndex = directiveEnd- 扫描:
tViewA.data[18]= CardTheme 的 VP →token === providerTokenOrDef✓ - 返回 18
getNodeInjectable(LView-A, tViewA, 18, tNode, flags)→ 执行 factory → 返回new CardTheme('light')
结果 :MyCard.theme 拿到 'light'。
13.27.5 第五步:如果换成 <span> 里的指令注入
现在假设 AppComponent 模板里 <span> 上挂了一个指令 HlDirective,它 inject(CardTheme):
css
app-root (LView-A)
└── my-card (LView-A 的节点,宿主 MyCard)
└── span (LView-A 的节点,宿主无组件)
<span> 的 TNode 在 AppComponent 的模板里,但 <span> 是 <my-card> 的投影内容。
查找从 <span> 的 TNode 开始:
步骤 1 :bloomHash = 5
步骤 2 :getInjectorIndex(spanTNode, LView-A) → span 没有 providers → -1
步骤 3 :① 起点调整:getParentInjectorLocation(spanTNode, LView-A)
spanTNode.parent= my-card 的 TNode,有 injectorIndex → 返回它,ViewOffset = 0previousTView = tViewA,injectorIndex= my-card 的索引,lView = LView-A
步骤 4:循环
bloomHasToken(5, myCardIdx, tViewA.data)→0b111000 & 0b100000→ truesearchTokensOnInjector(..., previousTView = tViewA, ...)currentTView = LView-A[TVIEW] = tViewAcanAccessViewProviders:previousTView != null→ 分支 B:previousTView != currentTView && ...tViewA != tViewA→ false- →
canAccessViewProviders = false✗
locateDirectiveOrProvider(..., false, false)startingIndex = 18 + 1 = 19(跳过 viewProviders 段)- 扫描 19.. → 找不到 CardTheme(只有 my-card 的
providers: [CardLogger],没有 CardTheme) - 返回
null
- →
NOT_FOUND
步骤 5:继续向上
parentLocation = LView-A[myCardIdx + 8]→ app-root 的索引(ViewOffset = 0)shouldSearchParent(flags, ...)→ truebloomHasToken(5, myCardIdx, LView-A)→ 查 cumulative =0b0111,掩码0b100000。0b0111 & 0b100000 = 0→ false ✗- →
injectorIndex = -1,剪枝,退出循环
步骤 6 :return notFoundValue(= NOT_FOUND)→ 回到 getOrCreateInjectable → 落到 lookupTokenUsingModuleInjector → 环境注入器里也没有 CardTheme → 抛 NullInjectorError
13.27.6 这个推演说明了什么
-
投影内容看不见宿主组件的
viewProviders------ 因为previousTView == currentTView(都在父模板里),分支 B 不成立。这是 §13.17.1 分支 B 那个(tNode.type & TNodeType.AnyRNode) !== 0条件的前半部分在起作用。 -
cumulative bloom 在这里帮了倒忙?不,它帮了大忙 ------
bloomHasToken(5, myCardIdx, LView-A)返回 false,直接剪掉了整条祖先链。如果没有这一步,代码会继续爬到 app-root、爬到根,每层做一次线性扫描。但要注意:my-card 的 cumulative 是
0b0111(app-root 的 token),里面没有 bit 5(CardTheme 的 VP)------因为 viewProviders 属于 my-card 的 node bloom,不属于任何人的 cumulative。这是一个精确的设计 :
viewProviders被有意排除在 cumulative bloom 之外,从而保证"往上爬的人找不到别人的 viewProviders"。剪枝在这里不是副作用,是语义的一部分。 -
ApiService的注入零位运算 ------inject(ApiService)时bloomHashBitOrFactory返回undefined(没有__NG_ELEMENT_ID__),lookupTokenUsingNodeInjector的else if (typeof bloomHash === 'number')直接不成立,落到return notFoundValue→ 模块注入器。在 10 层深的组件树里也是 0 次位运算。
13.27.7 一个反例:把它加进 providers 会发生什么
如果把 CardTheme 也加进 MyCard 的 providers:
ts
@Component({
providers: [CardLogger, {provide: CardTheme, useValue: new CardTheme('dark')}],
viewProviders: [{provide: CardTheme, useValue: new CardTheme('light')}],
})
resolveProvider 会:
- 先处理 VP:在段 1 写入
CardTheme(索引 18),cptViewProvidersCount++→ 1 - 再处理 providers:
indexOf(CardTheme, tInjectables, beginIndex + 1, endIndex)→ 从段 2 开始查 → 找不到 (段 2 是空的)→ 在索引 19 写入第二个CardTheme
于是 tView.data 的槽位:
arduino
18: CardTheme ← viewProviders 的('light')
19: CardTheme ← providers 的('dark')
两者互不覆盖 ,因为 indexOf 的查找范围被 cptViewProvidersCount 隔开了。
- 组件自己(
canAccessViewProviders = true)→ 从 18 开始扫 → 拿到'light' - 投影内容 / 外部(
canAccessViewProviders = false)→ 从 19 开始扫 → 拿到'dark'
这就是"同一个 token 在同一个节点上可以有两个值"的实现。§13.8.3 的那个官方例子用的正是这个机制。
13.28 关键源码位置速查
| 主题 | 文件 | 关键行 |
|---|---|---|
Injector 抽象类 |
core/src/di/injector.ts |
全文 147 行 |
NullInjector |
core/src/di/null_injector.ts |
全文 29 行 |
R3Injector |
core/src/di/r3_injector.ts |
194(class)、328(get)、529(hydrate) |
Record 三态 |
core/src/di/r3_injector.ts |
89(NOT_YET)、98(CIRCULAR)、116 |
providerToFactory |
core/src/di/r3_injector.ts |
644 |
| multi 处理 | core/src/di/r3_injector.ts |
502(env 层);render3/di_setup.ts 148(node 层) |
injectableDefInScope |
core/src/di/r3_injector.ts |
556 |
| inject 实现切换 | core/src/di/inject_switch.ts |
全文 81 行 |
| 注入上下文 | core/src/di/contextual.ts |
35(runInInjectionContext)、63(isInInjectionContext) |
injectAsync |
core/src/di/inject_async.ts |
37(v22 新增) |
NodeInjectorOffset |
core/src/render3/interfaces/injector.ts |
55 |
RelativeInjectorLocationFlags |
core/src/render3/interfaces/injector.ts |
73 |
NodeInjectorFactory |
core/src/render3/interfaces/injector.ts |
155 |
| bloom 常量 | core/src/render3/di.ts |
128(BLOOM_SIZE)、136(BLOOM_BUCKET_BITS) |
bloomAdd |
core/src/render3/di.ts |
152 |
getOrCreateNodeInjectorForNode |
core/src/render3/di.ts |
193 |
getParentInjectorLocation |
core/src/render3/di.ts |
258 |
lookupTokenUsingNodeInjector |
core/src/render3/di.ts |
506 |
searchTokensOnInjector |
core/src/render3/di.ts |
627 |
locateDirectiveOrProvider |
core/src/render3/di.ts |
685 |
getNodeInjectable |
core/src/render3/di.ts |
735 |
bloomHashBitOrFactory |
core/src/render3/di.ts |
825 |
bloomHasToken |
core/src/render3/di.ts |
849 |
shouldSearchParent |
core/src/render3/di.ts |
870 |
providersResolver |
core/src/render3/di_setup.ts |
56 |
resolveProvider |
core/src/render3/di_setup.ts |
70 |
| multi 双工厂 | core/src/render3/di_setup.ts |
320、336 |
providers/viewProviders feature |
core/src/render3/features/providers_feature.ts |
46 |
| 指令注册到 bloom | core/src/render3/view/directives.ts |
158 |
ɵɵdirectiveInject |
core/src/render3/instructions/di.ts |
41 |
13.29 本章核心结论
-
两套注入器解决两个正交问题:环境注入器管"应用级单例",节点注入器管"与 DOM 节点同寿的实例"。数据结构完全不同(Map vs 9 个数组槽位)。
-
bloom filter 的价值不在"快",在"剪枝":256 bit 的两个 bloom 让"祖先链上有没有这个 token"变成一次位运算,避免 O(depth) 的线性爬升。
-
bloomHashBitOrFactory的三态返回值是全章最优雅的设计 :返回数字走 bloom、返回函数直接调用、返回undefined直接短路。三种语义用一个返回值类型表达。 -
所有的"看不见"都靠区间控制 :
viewProviders不可见 =locateDirectiveOrProvider的startingIndex跳过一段;@Host=endIndex收窄到 viewProviders 段末尾;@Self=shouldSearchParent恒 false。 -
includeViewProviders是一个全局变量 ,跟着"当前正在实例化的服务被声明在哪儿"走。这是providers里的服务看不见viewProviders的全部实现。 -
循环依赖检测有两套 :
R3Injector用CIRCULAR哨兵对象,NodeInjector用factory.resolving布尔量。思想相同("进行中"占位),载体不同。 -
multi provider 在两层有完全不同的实现 :环境层用"provider 对象自身当 key",节点层用"双工厂 +
componentProviders计数器"保证顺序。 -
树摇的实质是把决定权从配置数组转移到引用图 :
providedIn让未使用的服务能被静态删除,而providers: []数组强制保留引用。 -
Object.hasOwn而不是in------ 防止子类继承父类的__NG_ELEMENT_ID__导致查错 token。 -
DI 全程
setActiveConsumer(null)------ 服务不是响应式的,工厂函数里读 signal 不建依赖边。
13.30 思考题
bloomAdd里1 << bloomHash当bloomHash >= 32时会溢出成1 << (bloomHash % 32),为什么不出错?- 如果一个应用里有 300 个不同的 provider token,第一个和第 301 个的
__NG_ELEMENT_ID__分别是多少?它们的bloomHash相同吗?会出什么问题? inject(ApiService)(providedIn: 'root')在一个 10 层深的组件里调用,会做几次 bloom 位运算?为什么?- 为什么
getOrCreateNodeInjectorForNode计算 cumulative bloom 时只 or 了父的两份数组,没有 or 自己的 node bloom? @Component({providers: [S], viewProviders: [{provide: T, useValue: 1}]})------S的构造函数里inject(T)能拿到吗?组件自己的构造函数呢?组件模板里的<div>上挂的指令呢?- multi provider 的
componentProviders计数器如果算错了,会出现什么现象? inject(Injector)和inject(EnvironmentInjector)有什么不同?在什么情况下后者会失败?- 为什么
runInInjectionContext里要setInjectImplementation(undefined)?如果不做会怎样? - 如果把
BLOOM_SIZE从 256 改成 512,需要改哪些地方?(提示:BLOOM_BUCKET_BITS、insertBloom、NodeInjectorOffset.BLOOM_SIZE) - 一个 token 的
__NG_ELEMENT_ID__是undefined,但它在某个组件的providers里被声明过------这可能吗?为什么?
下一章预告 :第 14 章《依赖注入(二):多级注入器、
providedIn树摇与实战》。本章讲的是"数据结构和算法",下一章讲"设计"------环境注入器的层次如何搭建(platform / root / route / component)、importProvidersFrom如何展平 NgModule、@Injectable编译器产物长什么样、以及如何用这些知识诊断真实的 DI 问题("为什么我拿到了两个实例""为什么懒加载后 service 被重建")。