Angular 依赖注入(DI)模块深度解析:Injector、Binding 与分层依赖解析完整指南

发布时间:2026/9/10 16:24:22
Angular 依赖注入(DI)模块深度解析:Injector、Binding 与分层依赖解析完整指南 Angular 依赖注入DI模块深度解析Injector、Binding 与分层依赖解析完整指南【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular导读本文以仓库设计文档 packages/docs/di/di.md 为骨架系统讲解 Angular 依赖注入模块的底层机制Injector注入器、Binding绑定、Dependency依赖三大核心抽象如何协同工作注入器为何惰性实例化 缓存单例父子注入器如何沿树向上解析依赖以及Self/Host/SkipSelf等约束如何精确划定查找边界。读完本文你将既能读懂设计层面的经典 API 语义bind(Car).toClass(...)这一 DSL 体系也能对应到当前仓库 packages/core/src/di 中useClass/useValue/useFactory/useExisting等现代 Provider 配置的真实实现形成从架构设计到源码验证的完整认知。说明packages/docs/di目录是 Angular 早期的 DI 架构设计文档其中大量使用了bind(...).toXxx(...)这一原型风格 DSL。虽然当前angular/core已改用具名属性useClass等的 Provider 对象语法但文档所描述的注入器语义、分层解析算法与依赖约束在今天 packages/core/src/di 的实现中依然成立。本文在保留原文档示例的同时逐一对齐现代 API。一、核心抽象Injector、Binding、Dependency设计文档开宗明义整个 DI 模块建立在Injector、Binding、Dependency三个核心抽象之上。Injector注入器由一组 Binding 创建而来它负责解析依赖、创建对象并缓存创建过的实例。Binding绑定把某个Token令牌例如一个字符串或一个类映射到工厂函数FactoryFn与一组Dependency。通俗地说Binding 定义了某个东西如何被创建。Dependency依赖指向某个 Token并携带该 Token 对应对象应如何被注入的附加信息即注入 Flag/约束。原文档用下面这张类关系图概括它们之间的组合关系*表示一对多[Injector] | | |* [Binding] |----------|-----------------| | | |* [Token] [FactoryFn] [Dependency] |---------| | | [Token] [Flags]把这张图翻译成一句通俗的话一个注入器持有若干绑定每条绑定把一个令牌映射为工厂函数 若干依赖每个依赖又指向另一个令牌并附带了如何注入的旗标。整个 DI 引擎正是在这组递归关系上运转的。其中Dependency里的Flags字段在 packages/core/src/di/interface/injector.ts 中演化为位掩码枚举InternalInjectFlagsDefault 0b0000 // 检查自身与父级注入器 Host 0b0001 // 向上查找直到宿主host元素为止 Self 0b0010 // 不向上查找祖先注入器 SkipSelf 0b0100 // 跳过发起注入的节点 Optional 0b1000 // 找不到时注入默认值null ForPipe 0b10000 // 内部专用该令牌被注入到管道中这些位值会在下文约束依赖查找范围一节逐一体现。注意ForPipe是编译器内部添加的旗标并不属于开发者可使用的公开InjectOptions。二、最小示例从绑定注册到依赖注入文档给出的经典示例是Engine与Carclass Engine { } class Car { constructor(Inject(Engine) engine) { } } var inj Injector.resolveAndCreate([ bind(Car).toClass(Car), bind(Engine).toClass(Engine) ]); var car inj.get(Car);这段代码蕴含了三步注册绑定bind(Car).toClass(Car)建立Car这个令牌 → 用Car类反射出的工厂来实例化的映射Engine同理。创建注入器Injector.resolveAndCreate([...])先解析传入的所有绑定再据此构建注入器。解析实例inj.get(Car)触发Car的实例化。Car构造函数上声明了Inject(Engine)于是注入器先解析出Engine再把它作为构造参数传给Car。Inject(Engine)的作用是把Engine这个令牌显式地声明为构造参数依赖。在文档对应的现代实现中Inject装饰器定义于 packages/core/src/di/metadata.ts它通过makeParamDecorator生成并由attachInjectFlag(..., DecoratorFlags.Inject)打上这是一个Inject装饰器的内部标记DecoratorFlags.Inject -1以保证可 tree-shaking。三、Injector 的惰性实例化与缓存文档强调注入器只在你真正索要对象时才去实例化它而且一旦创建就缓存起来。三个对照实验最能说明这一点先要 Car再要 Engine —— 两者都被新建var car inj.get(Car); //instantiates both an Engine and a Car先要 Engine再要 Car —— Engine 被复用var engine inj.get(Engine); //instantiates an Engine var car inj.get(Car); //instantiates a Car (reuses Engine)先要 Car再要 Engine —— Engine 从缓存读取var car inj.get(Car); //instantiates both an Engine and a Car var engine inj.get(Engine); //reads the Engine from the cache由此文档给出两条实践忠告要避免缺陷注册对象的构造函数应当无副作用side-effect-free。当构造函数纯粹无副作用时注入器表现得像一张哈希表——对象被创建的顺序不影响任何结果无论先取谁后取谁行为一致、可预测。因为每个绑定在注入器内只有一份实例inj.get(MyClass) inj.get(MyClass)永远成立详见瞬态依赖一节。这条懒加载 单例缓存语义也是现代angular/core注入器一贯的行为对同一个令牌的多次injector.get(token)返回同一实例。四、分层注入器与依赖解析算法注入器构成树状结构注入器是有层级的hierarchical。文档用父子注入器示例var parent Injector.resolveAndCreate([ bind(Engine).toClass(TurboEngine) ]); var child parent.resolveAndCreateChild([Car]); var car child.get(Car); // uses the Car binding from the child injector and Engine from the parent injector.Car在子注入器里解析而它依赖的Engine并没有注册在子注入器——于是查找顺着父链来到parent找到TurboEngine并成功注入。多个这样的父子关系构成一棵注入器树GrandParentInjector / \ Parent1Injector Parent2Injector | ChildInjector在 packages/core/src/di 的现代实现中树的根就是NullInjectorInjector.NULL见 packages/core/src/di/injector.ts——它始终查不到任何令牌并抛出未找到错误作为解析链的终点哨兵。解析算法只向上、逐级询问文档给出了伪代码形式的解析算法其逻辑至今未变// this is pseudocode. var inj this; while (inj) { if (inj.hasKey(requestedKey)) { return inj.get(requestedKey); } else { inj inj.parent; } } throw new NoProviderError(requestedKey);因此当解析Car的依赖Engine时DI从Car绑定所在的同一个注入器开始先问自己有没有Engine有就直接返回该实例没有就问父注入器如此一直向上直到命中Engine或到达根注入器。若到达根仍未命中则抛出找不到 Provider错误——在现代 Angular 中对应R3Injector/环境注入器的NG0201: No provider for X运行时错误。DI 不会向下查找需要特别警惕依赖解析只向上走绝不向下搜索子注入器。文档用下面这个必抛异常的例子说明var parent Injector.resolveAndCreate([Car]); var child parent.resolveAndCreateChild([ bind(Engine).toClass(TurboEngine) ]); parent.get(Car); // will throw NoProviderErrorparent自身没有Engine绑定而Engine只存在于它的子注入器child中——解析从parent出发只能向上永远看不到child因此必然抛NoProviderError。理解这一点是避免明明注册了却报找不到 Provider这类问题的关键依赖必须注册在被请求注入器的自身或其祖先上。五、用 Self / Host / SkipSelf 约束查找范围默认解析是从当前注入器一直向上到根。为了精细控制文档引入上界与下界约束upper/lower bound constraints对应上述Flags中的若干位。Self只看自身Self让 DI 只在Car定义的同一个注入器里找Engine完全不向上走class Car { constructor(Self() e: Engine){} }Self对应InternalInjectFlags.Self 0b0010Dont ascend to ancestors实现见 packages/core/src/di/interface/injector.ts。Host向上直到宿主Host告诉 DI在本注入器及其父级中查找Engine但一旦到达宿主host就停止host 语义详见下述可见性高级话题class Car { constructor(Host() e: Engine){} }Host对应InternalInjectFlags.Host 0b0001注释明确其仅在与 Element Injector 配合时生效packages/core/src/di/interface/injector.ts。文档还提示了一个更贴近实战的Host场景两个必须成对提供的绑定例如NgModel与NgRequiredValidator——约束它们必须在同一宿主组件范围内共同解析避免从树外误取。SkipSelf从父注入器开始向上SkipSelf让 DI跳过当前注入器从父注入器开始在整个祖先链上查找Engineclass Car { constructor(SkipSelf() e: Engine){} }SkipSelf对应InternalInjectFlags.SkipSelf 0b0100packages/core/src/di/interface/injector.ts。它在需要获取父级提供的同名服务、而不是被当前节点覆盖的那个实例时尤其有用。现代等价物与可选注入在现代angular/core中这四个装饰器定义于同一个文件 packages/core/src/di/metadata.tsOptional对应InternalInjectFlags.Optional、SelfInternalInjectFlags.Self、SkipSelfInternalInjectFlags.SkipSelf、HostInternalInjectFlags.Host全部经由makeParamDecorator(...)与attachInjectFlag(...)生成分别在第 115、162、208、250 行附近完成旗标绑定。此外函数式注入 API 也提供等价选项inject(token, {self: true | skipSelf: true | host: true | optional: true})其选项接口InjectOptions定义于 packages/core/src/di/interface/injector.ts。其中optional: true在找不到令牌时返回null而不是抛错对应位掩码中的Optional。六、Bindings类的四种绑定方式与别名文档把绑定方式归纳为绑定到类 / 绑定到值 / 绑定到工厂 / 绑定到别名四类并给出完整的对照写法。绑定到类toClassvar inj Injector.resolveAndCreate([ bind(Car).toClass(Car), bind(Engine).toClass(Engine) ]);以及它的语法糖——直接把类放进数组等价于bind(X).toClass(X)var inj Injector.resolveAndCreate([ Car, // syntax sugar for bind(Car).toClass(Car) Engine ]);这一语法糖在现代代码中依然保留Provider 数组中的裸类TypeProvider见 packages/core/src/di/interface/provider.ts会被当作用该类自身作为令牌并实例化它。绑定到值toValuevar inj Injector.resolveAndCreate([ bind(Car).toValue(new Car(new Engine())) ]);toValue直接把一个现成对象实例与令牌绑定注入器不做任何构造原样返回该对象。绑定到工厂toFactoryvar inj Injector.resolveAndCreate([ bind(Car).toFactory((e) new Car(e), [Engine]), bind(Engine).toFactory(() new Engine()) ]);toFactory(fn, deps)的第一个参数是工厂函数第二个参数deps是依赖令牌列表——工厂函数按顺序收到这些已解析好的依赖作为入参。因此工厂既能拿到别的服务(e) new Car(e)也能实现完全自控的实例化逻辑() new Engine()。绑定任意令牌Token 与工厂解耦令牌不一定非得是类字符串同样合法var inj Injector.resolveAndCreate([ bind(Car).toFactory((e) new Car(), [engine!]), bind(engine!).toClass(Engine) ]);这里工厂(e) new Car()声明依赖字符串令牌engine!engine!又绑定到Engine类。文档特别点明一个设计事实令牌与工厂函数是解耦的——bind(some token).toFactory(someFactory);someFactory完全不需要知道自己是在为some token生产对象反过来同一个工厂也可能被不同令牌复用。DI 通过令牌 →工厂 依赖→ 实例的两层映射把如何构造与为谁构造彻底分离。绑定别名toAlias想要让两个令牌指向同一个现有绑定的实例使用toAliasvar inj Injector.resolveAndCreate([ bind(Engine).toClass(Engine), bind(engine!).toAlias(Engine) ]);其语义是严格的同一性inj.get(Engine) inj.get(engine!)成立——两者返回同一个对象而不是两个副本。与现代 Provider 配置的对照表设计文档中的bind(...).toXxx(...)DSL 在当前仓库 packages/core/src/di/interface/provider.ts 中演化为等价的具名 Provider 对象表格中的provide即文档里的令牌经典 DSL本文档现代等价 Provider定义处packages/core/src/di/interface/provider.tsbind(Car).toClass(Car){provide: Car, useClass: Car}ClassProvider/StaticClassProvider第 74、91 行附近静态场景另支持deps[Car, Engine]语法糖Car, EngineTypeProviderTypeProvider第 280 行bind(Car).toValue(obj){provide: Car, useValue: obj}ValueProvider/ValueSansProvider第 17、40 行附近bind(Car).toFactory(fn, deps){provide: Car, useFactory: fn, deps: [...]}FactoryProvider/FactorySansProvider第 204、219 行附近bind(Engine).toClass(Engine) 手动 new{provide: X, useClass: Y, deps: [...]}或ConstructorProviderConstructorSansProvider第 118 行附近bind(engine!).toAlias(Engine){provide: engine!, useExisting: Engine}ExistingProvider/ExistingSansProvider第 161、169 行附近其中useExisting与toAlias完全同义injector.get(useExisting 的令牌)返回useExisting所指令牌的同一实例。至于字符串令牌现代最佳实践是改用类型安全的InjectionToken以避免命名冲突。七、Resolved Bindings预解析与性能当 DI 收到bind(Car).toClass(Car)时在能创建实例前必须先做两件准备工作反射Car生成工厂函数——把类翻译成如何构造的可执行描述规范化依赖列表——例如把每个依赖的上界/下界约束Self、SkipSelf等旗标预先计算并固化。这两步的产物称为Resolved Binding。Injector.resolveAndCreate/Injector.resolveAndCreateChild都是先解析绑定、再建注入器也就是说它们在创建注入器之前已经执行了上面的解析过程。但文档提醒你也可以手动预先解析var inj Injector.resolveAndCreate([ bind(Car).toClass(Car), bind(Engine).toClass(Engine) ]);手动解析再建注入器的完整流程如下——先用Injector.resolve(...)把 Provider 列表转成 Resolved 形式再交给fromResolvedProviders与createChildFromResolvedProvidersvar listOfResolvingProviders Injector.resolve([Provider1, Provider2]); var inj Injector.fromResolvedProviders(listOfResolvingProviders); inj.createChildFromResolvedProviders(listOfResolvedProviders);用预解析绑定创建注入器会更快解析只需做一次之后每次基于同一组 Resolved Binding 实例化注入器都直接复用结果。文档建议这类路径保留给对性能敏感的区域使用例如每元素高频创建注入器的场景。这一思想在当前实现中同样有迹可循Injector.resolveAndCreate这类便捷入口最终都收敛到核心的注入器构造逻辑而面向性能的路径则强调一次解析、多次实例化的分离参见 packages/core/src/di/create_injector.ts 与现代运行时注入器 packages/core/src/di/r3_injector.ts。八、Transient Dependencies如何每次拿到新实例文档明确指出一个前提一个注入器对每条注册的绑定只保留一份实例。inj.get(MyClass) inj.get(MyClass); //always holds如果业务上需要瞬态依赖——即每次获取都得到全新实例——文档给出两种方案方案一为每次新实例创建子注入器var child inj.resolveAndCreateChild([MyClass]); child.get(MyClass);每次resolveAndCreateChild都生成一个全新子注入器MyClass在其中自然只被创建一次需要 N 个实例就建 N 个子注入器。方案二注册一个每次调用都new的工厂函数var inj Injector.resolveAndCreate([ bind(MyClassFactory).toFactory(dep () new MyClass(dep), [SomeDependency]) ]); var factory inj.get(MyClassFactory); var instance1 factory(), instance2 factory(); // Depends on the implementation of MyClass, but generally holds. expect(instance1).not.toBe(instance2);这个写法很精妙注入器把MyClass的依赖SomeDependency注入到外层工厂dep () new MyClass(dep)外层工厂返回一个闭包函数注入器只缓存这个闭包工厂而每次调用闭包都会new一个全新的MyClass。因此instance1与instance2是不同的对象文档也严谨地加了注释是否必然不同取决于MyClass的具体实现但通常成立。方案二正是现代 DI 中useFactory返回生产器/工厂模式的雏形适合对象生命周期短、需要按需频繁新建的场景。九、总结把设计文档与当前源码接起来把packages/docs/di/di.md这份设计文档通读一遍可以提炼出六条至今仍然成立的 DI 心智模型令牌驱动一切依赖都以令牌类、字符串或InjectionToken为键令牌与工厂解耦惰性实例化 单例缓存只在get时才构造构造一次缓存复用构造函数应无副作用树状分层、只向上解析子注入器可补注册自己的绑定解析失败才逐级询问父注入器到根仍无则抛NoProviderError约束限定可见域Self/Host/SkipSelf/Optional通过位掩码旗标精确控制查找范围实现见 packages/core/src/di/interface/injector.ts绑定方式四选一类 / 值 / 工厂 / 别名现代分别对应useClass/useValue/useFactory(deps)/useExisting解析与实例化分离Resolved Binding 可预解析复用是注入器高效创建的基石。如果希望进一步理解宿主与可见性public / private / public-and-private、ProtoInjector与注入器分层实例化的高级机制可直接接着阅读同目录下的进阶文档 packages/docs/di/di_advanced.md若要看运行时真实代码现代实现集中在 packages/core/src/di注入器入口 packages/core/src/di/injector.ts、Provider 类型定义 packages/core/src/di/interface/provider.ts、运行时注入器 packages/core/src/di/r3_injector.ts。理解了这套从设计到实现的完整链路你在排查找不到 Provider、设计跨模块服务层级、控制服务作用域时就能有的放矢。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考