面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度,提高扩展性。
控制反转 IOC
Inversion of Control
用自己的话来说就是,对象的依赖,不再由自己创建(不用自己new,以及挂载等等操作)
而是自己声明一下依赖就行
对象会有一个 ioc 容器工作:把声明的依赖 实例化或者引用,挂载到自身... ...
所以 angular 可以对依赖进行 this.xxxService
通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。
也可以说,依赖被注入到对象中。
控制反转是一种思想,依赖注入是一种设计模式
实现:
(1)依赖查找(是一种更加传统的 IOC 实现方式)
依赖容器和标准 api 实现,容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象
缺点: 无法在容器外使用和测试对象
- 依赖拖拽
查找的过程是 从集中注册表中 进行的
- 上下文依赖查找
查找的过程是在 容器管理的资源中 进行的
(2)依赖注入 DI Dependency-injection-in-JavaScript
栗子01:
-
console.log(' ******************** test ********************'); (function() { // 球队信息 class RocketTeam { public name: string; constructor() { this.name = '火箭' } } // 球员信息 class Player { public team: RocketTeam; constructor() { this.team = new RocketTeam() } getTeamInfo() { console.log(this.team.name) } } let ym = new Player(); ym.getTeamInfo(); // 火箭 })();
- 可扩展性是很差
假如这个时候, 球员发生交易了,球队信息更换了,转换到 FireTeam了。
这时候我们就需要去修改 Player 里的代码了,因为 Player 那里直接写死了对 RocketTeam 的依赖
重新思考下依赖关系处理:
- 球员和球队之间非得这么直接粗暴的发生联系吗?
一个球员对应一个球队的话,未来会发生变化的可能性太大了,毕竟不止一个球队。
- 如果两者之间不直接发生联系,中间就需要一个中间模块来负责两者关系的处理
哪支球队控制权应该直接落在 Player 这里了,这正是 IOC 的设计思路
根据 IOC 原则 进行改进(目的: 降低耦合,提高扩展性)
(1)高层模块不应该依赖低层模块
这里 Player 是高层模块,直接依赖了 RocketTeam 这个低级模块。
所以我们将两者解耦,Player不再直接依赖于该 RocketTeam 这个 class
(2)抽象不应该依赖具体实现,具体实现应该依赖抽象
Player 模块不应该直接依赖具体 RocketTeam,而是通过构造函数将抽象的 TeamInfo 实例传递进去,这样就解耦具体实现
(3)面向接口编程,而非面向实现编程
-
console.log(' ******************** test ********************'); (function() { // 球队信息 class TeamInfo { public name: string; constructor(name: string) { this.name = name } } // 球员信息 class Player { public team: TeamInfo; constructor(team: TeamInfo) { this.team = team } getTeamInfo() { console.log(this.team.name) } } // 将依赖关系放到此处来管理,控制权也放到此处 // Player 和 TeamInfo 之间不再有直接依赖:Player 不再直接依赖掌握 TeamInfo 的控制权 // 将依赖控制,落在此处(第三方模块专门管理)即为控制反转 let ym = new Player(new TeamInfo('火箭')); ym.getTeamInfo(); const kobe = new Player(new TeamInfo('湖人')); kobe.getTeamInfo(); })();
这样再增加一个 team3,改动也不大,复用就行了。
- 将应用逻辑分解为服务, 让组件更加直观的使用,而无须关心实现
- 提高了模块化程度
- 解耦,增强了扩展性
- 代码复用
就是一个对象 a,依赖于一个对象 b
假设很多个对象,都依赖 b 对象,为了代码复用以及扩展性,把 b 抽象为一个 class B
class B 需要实例化成对象,才能被使用
依赖注入解决的就是:
1. 在对象 a 里面,如何实例化 依赖class B
2. 对象 a 什么时候实例化 依赖class B
3. 对象 a 如何实例化 依赖 B C D 这样的多依赖
4. 以及依赖的实现是单例还是共享
体验:
- 如果有 angular DI 你会看到:
constructor(private http: HttpClient) 这样的代码
- 假如没有Angular DI 机制,我们必须手动提供 HttpClient 来创建我们自己的服务
我们的代码会像这样:
我们需要获得 httpClient 对象
于是,我需要再实例一个 const httpClient = new HttpClient(httpHandler);
但 httpHandler 又从哪来?
然后 const myService = new MyService(httpClient);
如果这样创建下去,到底什么时候是个头。
而且,这个过程相当繁琐,而且很容易出错。
Angular 的 DI 机制自动地帮我们完成了上述的所有操作,我们所要做的只是在组件的构造函数中指定依赖项,组件将会很轻松地就能用到这些依赖
基本概念:
(1)注入器 injector
会创建依赖的实例
单例模式: 某个服务,在注入器中最多只有一个实例
不用自己创建注入器: Angular 会在启动过程中为你创建 全应用级注入器 以及所需的其它注入器
-
当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。
如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供者来制作一个,并把它加入注入器中,然后把该服务返回给 Angular
(2)装饰器 @Injectable()
可以被注入 依赖+ === 类可以被一个注入器实例化;
Angular 官方: @Injectable() 对所有服务都是必须的
(3)提供商 Provider
-
@Component({ //... // 在组件中配置注入器 providers: [ MyService ] //... })
配置注入器(告诉注入器 如何初始化 令牌(Token)所对应的依赖服务)
把令牌 (Token) 映射到工厂方法,依赖的实例对象就是通过这个方法创建的
老语法:在Angular 6 发布以前, 唯一的方法是在 providers: [] 中指定服务
根据具体使用场景, providers: [] 将有三种不同的用法:
(1)在预加载的模块的 @NgModule 装饰器中指定 providers: []
在这种情况下,服务将是全局单例的。
即使它被多个模块的 providers: [] 重复申明,它也不会重新创建实例。
注入器只会创建一个实例,这是因为它们最终都会注册到根级注入器。
(2)在懒加载的模块的 @NgModule 装饰器中指定 providers: []
在应用程序运行初始化后一段时间,懒加载模块中提供的服务实例才会在子注入器(懒加载模块)上创建。
如果在预加载模块中注入这些服务,将会报 No provider for MyService! 错误。
(3)在@Component和@Directive中使用providers: []
服务是按组件实例化的,并且可以在组件及其子树中的所有子组件中访问。
在这种情况下,服务不是单例的,每次我们在另一个组件的模板中使用组件时,我们都会获得所提供服务的新实例。
这也意味着服务实例将与组件一起销毁......
providedIn: 'root' 能在 @Component和 @Directive中使用吗? 不能,必须使用 provides 来为每个组件创建多服务对象
新语法:providedIn: 'root'
一个服务,只有被注入到某些组件或其他服务, 该服务才会打包服务代码。
缺点:
无助于我们定制服务
优势:
- 不需要在模块中导入这些服务
- 我们要做的仅仅是使用它们
在 providedIn 出现之前,公共服务
实现:需要在主模块的 providers: [] 中注入
代价:导致所有(可能的大量)的服务导入进该组件,即使我们只想使用其中一个服务
懒加载 providedIn: 'root' 解决方案:
如果我们在懒加载中使用 providedIn: 'root' 来实现服务会发生什么?
从技术上讲,'root'代表 AppModule ,但 Angular 足够聪明,如果该服务只是在惰性组件/服务中注入,那么它只会绑定在延迟加载的 bundle 中
如果我们又额外将服务注入到其他正常加载的模块中,那么该服务会自动绑定到 mian 的 bundle 中。
- 1、如果服务仅被注入到懒加载模块,它将捆绑在懒加载包中
- 2、如果服务又被注入到正常模块中,它将捆绑在主包中
代价:在拥有大量模块和数百项服务的大型应用程序中,它可能变得非常不可预测。
优化:加强模块的边界
(4)四种依赖注入方式
- 类 的依赖注入
服务:
-
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class HeroMessageService { heroMessages: string[] = [ 'root service' ]; constructor() { } }
组件:(注入根组件,引入 全局服务 单例对象)
-
import { Component, OnInit } from '@angular/core'; import {HeroMessageService} from '../services/hero-message/hero-message.service'; @Component({ selector: 'kjf-class-inject', templateUrl: './class-inject.component.html', styleUrls: ['./class-inject.component.scss'] }) export class ClassInjectComponent implements OnInit { constructor(public messageService: HeroMessageService) {} ngOnInit(): void {} }
- 值 的依赖注入 useValue
- 别名依赖 的依赖注入 useExisting
- 限定方式 的依赖注入
- @Optional() logger: LoggerService 可以兼容依赖不存在的情况,提高系统的健壮性
- @Host() logger: LoggerService 在父组件中查找依赖,父组件没有这个依赖,则报错
服务:
-
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class HeroMessageService { heroMessages: string[] = [ 'root service' ]; constructor() { } // 往缓存中添加一条消息 addHeroMessage(heroMessage: string) { this.heroMessages.push(heroMessage); setTimeout(() => this.clearHeroMessages(), 15000); } // 清空缓存 clearHeroMessages() { this.heroMessages = []; } }
组件:(即使新语法 providedIn:"root" 了,但是在组件中 provides 使得 HeroMessageService 并且将被作为组件级别单例提供)
-
import { Component, OnInit } from '@angular/core'; import { HeroMessageService } from '../services/hero-message/hero-message.service'; @Component({ selector: 'kjf-limit-inject', templateUrl: './limit-inject.component.html', styleUrls: ['./limit-inject.component.scss'], providers: [HeroMessageService] }) export class LimitInjectComponent implements OnInit { constructor(public messageService: HeroMessageService) {} ngOnInit(): void {} addMsg(event: Event) { return this.messageService.addHeroMessage(` '希望把服务的有效性限制到应用程序的一个特定区域,使用:', 'providers: [HeroService]', '通过把服务添加到子组件 @Component() 装饰器的 providers 数组中,来为 HeroesBaseComponent 提供另一个 HeroService 实例', '当 Angular 新建 HeroBaseComponent 的时候,它会同时新建一个 HeroService 实例,该实例只在该组件及其子组件(如果有)中可见', `); } }
注意:
(1)Angular注入器是冒泡机制的
- 当一个组件申请获得一个依赖时,Angular先尝试用该组件自己的注入器来满足它
- 如果该组件的注入器没有找到对应的提供商,它就把这个申请 转给它的父组件注入器来处理
- 如果它的父组件注入器也无法满足这个申请,它就继续转给它在注入器树中的父注入器
- 这个申请继续往上冒泡—直到 Angular 找到一个能处理此申请的注入器 或者 超出了组件树中的祖先位置为止
- 如果超出了组件树中的祖先还未找到,Angular 就会抛出一个错误
疑问:
1. 数据存到 service 感觉已经很好用了,但是 store 的存在,所以在想: 什么场景下,需要用 store 代替 service 存储状态数据
主要是处理了回调的问题,如果是 service,需要传 callBack
如果是 store 则可以使用 rxjs 那样观察订阅的方式
鸣谢:
https://segmentfault.com/a/1190000019500553 达观数据文章,让我收获颇多
https://www.juyifx.cn/article/678531135.html
https://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript
https://juejin.im/post/6844903698376720398
https://www.jianshu.com/p/1159cdddde11
https://www.jianshu.com/p/4b10948d456c
https://angular.cn/api/core/Injectable#providedIn