inject doesn't work? ! What is the implementation principle and operation logic behind dependency injection?

Public number business card Author business card

one question

first question

As shown in the figure above, let's think about a problem first. The host project uses the components in the business component library, and then injects a name called to datethe key, whose value is the current timestamp, ask the business component can Do you get the data injected by the host project?

Before answering this question, let's take a look at how provide and inject are used.

dependency injection

provide

To supply data to the descendants of the component, use the provide()function :

<script setup>
import { provide } from 'vue'

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
复制代码

If not used <script setup>, make sure provide()is setup()called synchronously:

import { provide } from 'vue'

export default {
  setup() {
    provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
  }
}
复制代码

provide()The function accepts two parameters. The first parameter is called the injection name and can be a string, number or Symbol. Descendent components will use the injection name to look up the value that is expected to be injected. A component can be called multiple times provide(), using different injection names, and injecting different dependency values.

The second parameter is the supplied value, which can be of any type, including reactive state, such as a ref:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)
复制代码

The reactive state of the provider allows descendant components to establish a reactive relationship with the provider from there.

Application layer provide

In addition to supplying data for a component, we can also supply it at the application level:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
复制代码

Application-level provisioning can be injected in all components of the application. This is especially useful when you're writing plugins, since plugins generally don't use components to supply values.

inject

To inject data supplied by an ancestor component, use the inject()function :

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>
复制代码

If the supplied value is a ref, it is injected into itself, not automatically unwrapped. This allows the injected component to maintain a responsive connection to the provider.

Similarly, if not used <script setup>, inject()it needs to be called setup()synchronously :

import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message')
    return { message }
  }
}
复制代码

Injected default value

默认情况下,inject 假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。

如果在供给的一侧看来属性是可选提供的,那么注入时我们应该声明一个默认值,和 props 类似:

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
复制代码

在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了 避免在不使用可选值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值

const value = inject('key', () => new ExpensiveClass())
复制代码

配合响应性

当使用响应式 provide/inject 值时,建议尽可能将任何对响应式状态的变更都保持在 provider 内部。 这样可以确保 provide 的状态和变更操作都在同一个组件内,使其更容易维护。

有的时候,我们可能需要在 injector 组件中更改数据。在这种情况下,我们推荐在 provider 组件内提供一个更改数据方法:

<!-- 在 provider 组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
复制代码
<!-- 在 injector 组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>
复制代码

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

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>
复制代码

使用 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<T>(key: InjectionKey<T> | 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<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: T,
  treatDefaultAsFactory?: false
): T
export function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: T | (() => T),
  treatDefaultAsFactory: true
): T

export function inject( key: InjectionKey<any> | 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 的源码之后,我们来分析一下文章开头提出的问题。

first question

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

第二个问题

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

second question

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

问题出在了哪里?

The problem lies in the Symbol. In fact, in this scenario, the Symbol introduced by the host project and the Symbol introduced by the business component library are not same Symbol , because the Symbol instances created in different applications are always unique .

If we want all applications to share a Symbol instance, at this time we need another API to create or obtain Symbol, that is Symbol.for(), it can register or obtain a window global Symbol instance.

Our public second-party library (common) only needs to be modified as follows:

export const date = Symbol.for('date');
复制代码

Summarize

If we want to inject the provide provided by the upper layer, we need to pay attention to the following points:

  • Make sure that inject and provide components are in the same component tree
  • If using Symbol as the key, make sure both components are in the same application
  • If the two components are not in the same application, use Symbol.for to create a global Symbol instance as the key value

refer to

[1]: Vue3.0 inject source codegithub.com/vuejs/core/…

For more exciting things, please pay attention to our public account "Hundred Bottles Technology", there are irregular benefits!

Guess you like

Origin juejin.im/post/7118926883647356942