inject 不生效?!依赖注入背后的实现原理和运行逻辑是怎样的?

公众号名片
inject 不生效?!依赖注入背后的实现原理和运行逻辑是怎样的?_第1张图片

一个问题

inject 不生效?!依赖注入背后的实现原理和运行逻辑是怎样的?_第2张图片

如上图所示,我们先来思考一个问题,宿主项目使用了业务组件库中的组件,然后在宿主项目中向业务组件注入了一个名为 datekey,其值为当前的时间戳,问 业务组件可以拿到宿主项目注入的数据吗?

在回答这个问题之前,我们先来看一下 provide 和 inject 的使用方式。

依赖注入

provide

要为组件后代供给数据,需要使用到 provide() 函数:

如果不使用

如果供给的值是一个 ref,注入进来的就是它本身,而 不会 自动解包。这使得被注入的组件保持了和供给者的响应性连接。

同样的,如果没有使用




最后,如果你想确保从 provide 传过来的数据不能被 injector 的组件更改,你可以使用 readonly() 来包装提供的值。

使用 Symbol 作为注入名

至此,我们已经了解了如何使用字符串作为注入名。但如果你正在构建大型的应用程序,包含非常多的依赖供给,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。

建议在一个单独的文件中导出这些注入名 Symbol:

export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, { /*
  要供给的数据
*/ });
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

实现原理

在对依赖注入有一个大致的了解之后我们来看一下其实现的原理是怎样的。直接上源码:

export function provide(key: InjectionKey | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // TS doesn't allow symbol as index type
    provides[key as string] = value
  }
}
export function inject(key: InjectionKey | string): T | undefined
export function inject(
  key: InjectionKey | string,
  defaultValue: T,
  treatDefaultAsFactory?: false
): T
export function inject(
  key: InjectionKey | string,
  defaultValue: T | (() => T),
  treatDefaultAsFactory: true
): T

export function inject(
  key: InjectionKey | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // fallback to `currentRenderingInstance` so that this can be called in
  // a functional component
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // #2400
    // to support `app.use` plugins,
    // fallback to appContext's `provides` if the instance is at root
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    if (provides && (key as string | symbol) in provides) {
      // TS doesn't allow symbol as index type
      return provides[key as string]
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue.call(instance.proxy)
        : defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  } else if (__DEV__) {
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}
源码位置:packages/runtime-core/src/apiInject.ts

先不管开头提出的问题,我们先来看一下 provide 的源码,注意下面这句代码:

if (parentProvides === provides) {
  provides = currentInstance.provides = Object.create(parentProvides);
}

这里要解决一个问题,当父级 key 和 爷爷级别的 key 重复的时候,对于子组件来讲,需要取最近的父级别组件的值,那这里的解决方案就是利用原型链来解决。

provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides,所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就说明是第一次做 provide(对于当前组件来讲),我们就可以把 parent.provides 作为 currentInstance.provides 的原型重新赋值。

至于为什么不在 createComponent 的时候做这个处理,可能的好处是在这里初始化的话,是有个懒执行的效果(优化点,只有需要的时候才初始化)。

看完了 provide 的源码,我们再来看一下 inject 的源码。

inject 的执行逻辑比较简单,首先拿到当前实例,如果当前实例存在的话进一步判断当前实例的父实例是否存在,如果父实例存在则取父实例的 provides 进行注入,如果父实例不存在则取全局的(appContext)的 provides 进行注入。

inject 失效?

在看完 provide 和 inject 的源码之后,我们来分析一下文章开头提出的问题。

inject 不生效?!依赖注入背后的实现原理和运行逻辑是怎样的?_第3张图片

我们在业务组件中注入了来自宿主项目的 provide 出来的 key,业务组件首先会去寻找当前组件(instance),然后根据当前组件寻找父组件的 provides 进行注入即可,显然我们在业务组件中是可以拿到宿主项目注入进来的数据的。

第二个问题

分析完了文章开头提出的问题,我们再来看一个有意思的问题。下图中的业务组件能拿到宿主项目注入的数据吗?

inject 不生效?!依赖注入背后的实现原理和运行逻辑是怎样的?_第4张图片

答案可能跟你想的有点不一样:这个时候我们就拿不到宿主项目注入的数据了!!!

问题出在了哪里?

问题出在了 Symbol 这里,事实上在这个场景下,宿主项目引入的 Symbol 和 业务组件库引入的 Symbol 本质上 并不是同一个 Symbol,因为在 不同应用中创建的 Symbol 实例总是唯一的

如果想要所有的应用共享一个 Symbol 实例,这个时候我们就需要另一个 API 来创建或获取 Symbol,那就是 Symbol.for(),它可以注册或获取一个 window 全局的 Symbol 实例。

我们的公共二方库(common)只需要做如下修改即可:

export const date = Symbol.for('date');

总结

我们要想 inject 上层提供的 provide 需要注意以下几点:

  • 确保 inject 和 provide 的组件在同一颗组件树中
  • 若使用 Symbol 作为 key 值,请确保两个组件处于同一个应用中
  • 若两个组件不处于同一个应用中,请使用 Symbol.for 创建全局的 Symbol 实例作为 key 值使用

参考

更多精彩请关注我们的公众号「百瓶技术」,有不定期福利呦!

你可能感兴趣的:(前端vue.js)