1 注入,一种组件树状层级通信模式 & 设计模式
1.1 组件通信模式
在Angular工程开发中,通常我们使用Input属性绑定和Output事件绑定进行组件通信,然而Input和Output却只能在父子组件中传递信息。组件根据调用关系形成一棵组件树,如果只有属性绑定和事件绑定,那么两个非直接关系组件要通信,需要通过各个连接点本身,中间人需要不断处理和传递一些它本身不需要知道的信息(如图1左)。而Angular中提供的Injectable的Service,可以在模块、组件或者指令等提供,搭配在构造函数的注入,正好能解决这个问题(图1右)。

图1 组件通信模式
左图只通过父子组件传递信息,节点a和节点b进行通信就需要经过诸多节点;如果节点c想要通过一些配置控制节点b,他们中间的节点也必须设置额外的属性或者事件来透传对应的信息。右图的依赖注入模式节点c可以提供一个供节点a、b通信的服务,节点a直接和节点c提供 服务通信,节点b也直接和节点c提供的服务通信,最后通信就被简化了,中间节点也没有耦合该部分内容,对上下层组件发生的通信无明显的感知。
1.2 使用依赖注入实现控制反转
依赖注入(DI)并不是Angular特有的,它是实现控制反转(IOC)设计模式的手段,依赖注入的出现解决手动实例化过分耦合的问题,所有资源不由使用资源的双方管理,而由不使用资源资源中心或者第三方提供,这样能带来很多好处。第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。
类比现实世界就是,我们去购买商品比如一支铅笔,我们只需要找个商店购买一支类型为铅笔的商品,我们不关心这支铅笔产地是哪里,木头和铅笔芯都是怎么粘合的,我们只需要它能完成铅笔的书写功能即可,我们不会和具体的铅笔制造商或者工厂有联系。而对于商店,它就可以自己去合适的渠道采购铅笔,实现资源的可配置。
结合编码场景,更具体的说,使用者不需要显式创建实例(new操作),就能注入并使用实例,实例的创建由提供商(providers)决定。资源的管理是通过令牌(token),由于不关心提供商,不关心实例的创建,使用方就可以通过一些局部注入的手段(对token进行二次配置),最终实现替换实例,依赖注入模式的应用和切面编程(AOP)相辅相成。
2 Angular中的依赖注入
依赖注入是Angular框架最重要几个的核心模块之一,Angular不仅提供Service类型的注入,本身组件树就是一颗注入依赖树, 函数和值也可以被注入。也就是说在Angular框架中,子组件是可以通过父组件的token(通常为类名),注入父组件实例的。在组件库开发中有大量案例是通过注入父组件,实现交互和通信的,包括参数挂载,状态共享,甚至获取父组件所在节点的DOM等等。
2.1 解析依赖
要使用Angular的注入,首先就要明白它的注入解析的过程。类似于node_modules的解析过程,当找不到依赖都有找不到依赖会一直冒泡到父层去找依赖。旧版(v6前)的Angular会将注入解析的过程分为多级模块注入器,多级组件注入器和元素注入器。新版(v9后)简化为两级模型,第一个查询链是静态DOM层级的元素注入器、组件注入器等统称为元素注入器,另一个查询链是模块注入器。解析的顺序和解析失败后的默认值官方的这个代码注释文档(provider_flag)里讲的比较清楚了。

图2 两级注入器查找依赖过程 ( 图片来源)
也就是说组件/指令以及在组件/指令层级提供注入内容会优先在组件视图中元素里寻找依赖一直到根元素,如果没有找到则接着在元素当前所在模块,引用(包含模块引用和路由懒加载引用)该模块的父级模块一次往上找直到根模块和平台模块。
注意这里注入器是有继承的,元素注入器可以创建并继承父元素的注入器的查找函数,模块注入器也类似。当不断继承之后,就有点像js对象的prototype链了。