精讀《Inject Instance 源碼》

1. 引言

本週精讀的源碼是 inject-instance 這個庫。前端

這個庫的目的是爲了實現 Class 的依賴注入。git

好比咱們經過 inject 描述一個成員變量,那麼在運行時,這個成員變量的值就會被替換成對應 Class 的實例。這等於讓 Class 具有了申明依賴注入的能力:github

import {inject} from 'inject-instance'
import B from './B'

class A {
  @inject('B') private b: B
  public name = 'aaa'

  say() {
    console.log('A inject B instance', this.b.name)
  }
}
複製代碼

試想一下,若是成員函數 b 是經過 New 出來的:數組

class A {
  private b = new B()

  say() {
    console.log('A inject B instance', this.b.name)
  }
}
複製代碼

這個 b 就不具有依賴注入的特色,由於被注入的 b 是外部已經初始化好的,而不是實例化 A 時動態生成的。微信

須要依賴注入的通常都是框架級代碼,好比定義數據流,存在三個 Store 類,他們之間須要相互調用對方實例:框架

class A {
  @inject('B') private b: B
}

class B {
  @inject('C') private c: C
}

class C {
  @inject('A') private a: A
}
複製代碼

那麼對於引用了數據流 A、B、C 的三個組件,要保證它們訪問到的是同一組實例 A B C 該怎麼辦呢?ide

這時候咱們須要經過 injectInstance 函數統一實例化這些類,保證拿到的實例中,成員變量都是屬於同一份實例:函數

import injectInstance from 'inject-instance'

const instances = injectInstance(A, B, C)
instances.get('A')
instances.get('B')
instances.get('C')
複製代碼

那麼框架底層能夠經過調用 injectInstance 方式初始化一組 「正確注入依賴關係的實例」,拿 React 舉例,這個動做能夠發生在自定義數據流的 Provider 函數裏:ui

<Provider stores={{ A, B, C }}>
  <Root /> </Provider>
複製代碼

那麼在 Provider 函數內部經過 injectInstance 實例化的數據流,能夠保證 A B C 操做的注入實例都是當前 Provider 實例中的那一份this

2. 精讀

那麼開始源碼的解析,首先是總體思路的分析。

咱們須要準備兩個 API: injectinjectInstance

inject 用來描述要注入的類名,值是與 Class 名相同的字符串,injectInstance 是生成一系列實例的入口函數,須要生成最終生效的實例,並放在一個 Map 中。

inject

inject 是個裝飾器,它的目的有兩個:

  1. 修改 Class 基類信息,使其實例化的實例能拿到對應字段注入的 Class 名稱。
  2. 增長一個字段描述注入了那些 Key。
const inject = (injectName: string): any => (target: any, propertyKey: string, descriptor: PropertyDescriptor): any => {
    target[propertyKey] = injectName

    // 加入一個標註變量
    if (!target['_injectDecorator__injectVariables']) {
        target['_injectDecorator__injectVariables'] = [propertyKey]
    } else {
        target['_injectDecorator__injectVariables'].push(propertyKey)
    }

    return descriptor
}
複製代碼

target[propertyKey] = injectName 這行代碼中,propertyKey 是申明瞭注入的成員變量名稱,好比 Class A 中,propertyKey 等於 b,而 injectName 表示這個值須要的對應實例的 Class 名,好比 Class A 中,injectName 等於 B

_injectDecorator__injectVariables 是個數組,爲 Class 描述了這個類參與注入的 key 共有哪些,這樣能夠在後面 injectInstance 函數中拿到並依次賦值。

injectInstance

這個函數有兩個目的:

  1. 生成對應的實例。
  2. 將實例中注入部分的成員變量替換成對應實例。

代碼不長,直接貼出來:

const injectInstance = (...classes: Array<any>) => {
    const classMap = new Map<string, any>()
    const instanceMap = new Map<string, any>()

    classes.forEach(eachClass => {
      if (classMap.has(eachClass.name)) {
        throw `duplicate className: ${eachClass.name}`
      }
      classMap.set(eachClass.name, eachClass)
    })

    // 遍歷全部用到的類
    classMap.forEach((eachClass: any) => {
      // 實例化
      instanceMap.set(eachClass.name, new eachClass())
    })

    // 遍歷全部實例
  instanceMap.forEach((eachInstance: any, key: string) => {
    // 遍歷這個類的注入實例類名
    if (eachInstance['_injectDecorator__injectVariables']) {
      eachInstance['_injectDecorator__injectVariables'].forEach((injectVariableKey: string) => {
        const className = eachInstance.__proto__[injectVariableKey];
        if (!instanceMap.get(className)) {
          throw Error(`injectName: ${className} not found!`);
        }

        // 把注入名改爲實際注入對象
        eachInstance[injectVariableKey] = instanceMap.get(className);
      });
    }

    // 刪除這個臨時變量
    delete eachInstance['_injectDecorator__injectVariables'];
  });

  return instanceMap
}
複製代碼

能夠看到,首先咱們將傳入的 Class 依次初始化:

// 遍歷全部用到的類
classMap.forEach((eachClass: any) => {
  // 實例化
  instanceMap.set(eachClass.name, new eachClass())
})
複製代碼

這是必須提早完成的,由於注入可能存在循環依賴,咱們必須在解析注入以前就生成 Class 實例,此時須要注入的字段都是 undefined

第二步就是將這些注入字段的 undefined 替換爲剛纔實例化 Map instanceMap 中對應的實例了。

咱們經過 __proto__ 拿到 Class 基類在 inject 函數中埋下的 injectName,配合 _injectDecorator__injectVariables 拿到 key 後,直接遍歷全部要替換的 key, 經過類名從 instanceMap 中提取便可。

__proto__ 僅限框架代碼中使用,業務代碼不要這麼用,形成額外理解成本。

因此總結一下,就是提早實例化 + 根據 inject 埋好的信息依次替換注入的成員變量爲剛纔實例化好的實例。

3. 總結

但願讀完這篇文章,你能理解依賴注入的使用場景,使用方式,以及一種實現思路。

框架實現依賴注入都是提早收集全部類,統一初始化,經過注入函數打標後全局替換,這是一種思惟套路。

若是有其餘更有意思的依賴注入實現方案,歡迎討論。

討論地址是:精讀《Inject Instance 源碼》 · Issue #176 · dt-fe/weekly

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章
相關標籤/搜索