注入が機能しませんか?!依存性注入の背後にある実装原理と操作ロジックは何ですか?

公番号名刺 著者の名刺

一つの質問

最初の質問

上の図に示すように、最初に問題について考えてみましょう。ホストプロジェクトは、ビジネスコンポーネントライブラリのコンポーネントを使用してから、ホストプロジェクトdatekeyの値は現在のタイムスタンプです。ビジネスコンポーネントは、ホストプロジェクトによって注入されたデータを取得できますか?

この質問に答える前に、提供と注入がどのように使用されるかを見てみましょう。

依存性注入

提供

コンポーネントの子孫にデータを提供するには、次のprovide()関数。

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

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

使用しない場合は、同期的<script setup>呼び出されることを確認してください。provide()setup()

import { provide } from 'vue'

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

provide()この関数は2つのパラメーターを受け入れます。最初のパラメータはインジェクション名と呼ばれ、文字列、数値、または記号にすることができます。子孫コンポーネントは、注入名を使用して、注入されると予想される値を検索します。コンポーネントはprovide()、異なるインジェクション名を使用し、異なる依存関係値をインジェクトして、複数回呼び出すことができます。

2番目のパラメーターは指定された値であり、ref:などのリアクティブ状態を含む任意のタイプにすることができます。

import { ref, provide } from 'vue'

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

プロバイダーのリアクティブ状態により、子孫コンポーネントはそこからプロバイダーとのリアクティブ関係を確立できます。

アプリケーション層は提供します

コンポーネントのデータを提供するだけでなく、アプリケーションレベルでデータを提供することもできます。

import { createApp } from 'vue'

const app = createApp({})

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

アプリケーションレベルのプロビジョニングは、アプリケーションのすべてのコンポーネントに注入できます。プラグインは通常、値を提供するためにコンポーネントを使用しないため、これはプラグインを作成するときに特に役立ちます。

注入する

祖先コンポーネントによって提供されたデータを注入するには、次のinject()関数。

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

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

指定された値がrefの場合、自動的にアンラップされるのではなく。これにより、注入されたコンポーネントはプロバイダーへの応答性の高い接続を維持できます。

同様に、使用しない場合は<script setup>同期的inject()に呼び出す必要があります。setup()

import { inject } from 'vue'

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

注入されたデフォルト値

默认情况下,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 的源码之后,我们来分析一下文章开头提出的问题。

最初の質問

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

第二个问题

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

2番目の質問

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

问题出在了哪里?

問題はSymbolにあります。実際、このシナリオでは、異なるホストプロジェクトによって導入されたSymbolとビジネスコンポーネントライブラリによって導入されたSymbolは本質的に同じSymbolではありません

すべてのアプリケーションでSymbolインスタンスを共有する場合は、この時点で、Symbolを作成または取得するための別のAPIが必要です。つまりSymbol.for()、ウィンドウのグローバルSymbolインスタンスを登録または取得できます。

パブリックセカンドパーティライブラリ(共通)は、次のように変更するだけで済みます。

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

要約する

上位層から提供された提供を注入する場合は、次の点に注意する必要があります。

  • インジェクトコンポーネントと提供コンポーネントが同じコンポーネントツリーにあることを確認してください
  • シンボルをキーとして使用する場合は、両方のコンポーネントが同じアプリケーションにあることを確認してください
  • 2つのコンポーネントが同じアプリケーションにない場合は、Symbol.forを使用してグローバルSymbolインスタンスを作成し、それをキー値として使用します

参照する

[1]:Vue3.0はソースコードを注入します github.com/vuejs/core/…

もっとエキサイティングなことについては、私たちのパブリックアカウント「HundredBottlesTechnology」に注意してください。不規則な利点があります!

おすすめ

転載: juejin.im/post/7118926883647356942