前端学 IOC(1)- 到底有啥用?

329 阅读4分钟

毕业学JAVA时第一次听到这个名词, IOC, DI, 那会听得云里雾里, spring什么的, 完全不知道干嘛的,反正就写接口呗, controller, service, dao, 一把梭写下来,也没感觉IOC有啥作用...

后来干前端了以后, IOC这个名词更是很少被提及, (nest.js里有,但是我很少写node) 最近在写项目时, 正好用到了IOC, 我们来系统学习一下这个概念

什么是IOC?

IOC的全称是 Inversion of Control ( 控制反转 ) 名字不太好理解 , 控制谁呢? 为啥要反转... 让我们来看下面的例子

例子中我们声明了一个类,程序员(Developer), 它有一个属性, vscode, 平时的工作就是将搜索出来的内容复制到 vscode 里...

vscode 这个类我们直接引入, 在构造这个 developer 的时候直接创建

import { VSCODE } from './vscode'

// 考虑这个例子,
class Developer() {
    vscode: VSCODE = new VSCODE()
    work: () {
        this.vscode.open()
        // searching code...
        this.vscode.run('ctrl + v', code)
    }
}

// 使用的时候
new Developer().work()

这么写当然一点问题没有, 但是考虑如下场景: 当 vscode 这个对象, 不再是自己维护呢?
例如: 我们正在编写一个框架, 而使用这个框架的用户, 需要定制自己的vscode, 那么我们不得不要求用户在创建 Developer 之前,先创建 vscode 然后作为构造参数传进来。

// 使用的时候
const vc = new VSCODE()
new Developer(vc).work()

当互相依赖的情况再复杂一点,很容易发生下面的情况

import A from './A'
import B from './B'
import C from './C'
import D from './D'

const a = new A()
const b = new B(a)
const c = new C(b)
const d = new D(c)
d.work() 
// 天哪, 我只是想简简单单创建一个 D, 却要引入一大堆文件, 创建一堆对象,
// 谁要是设计这样的框架让别人用, 不得被骂死

那么, 能不能在编写 Developer 的时候, 只是声明一下需要的依赖,由系统自动帮忙new出所需的依赖,然后自动注入呢? 理想的状态是:

import Container from 'Container'
// 理想状态
const developer = Container.get('Developer')  // 大致等同于 new Developer()
// Container 在 get 的时候, 会扫描 Developer 所需要的依赖, 然后率先创建, 自动拼装, 并正确传入

那么问题来了, Container 到底怎么知道 Developer 这个对象需要什么依赖呢? 当然不同的库有不同的 api , 我们以 inversify 为例 inversify.io/


import { Container, injectable, inject } from "inversify";

@injectable()
class Developer {
    // 这里就标记了 Developer 所需的依赖: vscode对象
    // 到时候由框架创建一个, 然后传进来就行, 编写 Developer 类的同学则完全不需要考虑  
    // 也无需 import 任何特定的 vscode 实现, 做到了与 vscode 完全解耦
    @inject("VSCODE")
    private vscode: VSCODE;
    
    work: () {
        this.vscode.open()
        // searching code...
        this.vscode.run('ctrl + v', code)
    }
}

// 这里可能由某个负责其他插件的同学来实现 vscode
@injectable()
class VSCODE {
    run: () {
        // do ...
    }
}

// 注册阶段
var container = new Container();
container.bind<Developer>('Developer').to(Developer);
container.bind<VSCODE>('VSCODE').to(VSCODE);

// 用户真正使用的时候
const developer = Container.get('Developer')  // 大致等同于 new Developer()
// 此时Container 根据之前扫描出的依赖图, 已经知道 developer 需要一个 vscode 依赖, 
// ioc框架会自动 new 一个Vscode对象然后帮你设置进 developer
// 而你作为用户, 这一切都是无感的, very nice

所以我们可以看到, 使用 IOC 至少有两个好处:

  1. 在我们编写类的时候, 如果依赖了其他类, 无需在本文件中写死, 而是声明即可, 到时候由会有框架帮你注入, 这样做到解耦, 也方便其他人扩展
  2. 在创建对象的时候, 如果互相的依赖错综复杂, 你无需安装顺序手动组装, 而是由框架按照扫描出的依赖图, 帮你组装完毕, 你直接使用即可

OK, 我们重新来理解 IOC —— Inversion of Control ( 控制反转 ) 到底反转了什么? 正常情况下, Developer类 依赖了 Vscode类, 那么在 developer的文件中将 vscode引入进来new一下即可, 那么控制权在 Developer, 具体创建哪个vscode都是developer说了算
使用 ioc 了之后, 创建 vscode 的控制权被交给了容器, 由容器框架来决定给develop注入哪个特定的vscode实现 这也是DI (DI—Dependency Injection) 依赖注入的意思

当然这里只是简要讨论了IOC的使用场景之一, 仍有许多问题没有回答:

  1. ioc框架究竟如何扫描出依赖图?
  2. 依赖图中会不会有循环依赖呢?
  3. 如果创建对象需要参数怎么办?
  4. 是否每次都需要创建全新的对象呢? 如果想复用之前的对象怎么办?