Vue3 封装知识点(三)依赖注入:provide 和 inject 详细介绍

一、provide 和 inject 是什么

父组件中提供数据,并在子组件中注入这些数据,从而实现了组件之间的数据传递。简单来说就是父组件向子组件传值的一个方式。

二、为了解决什么问题

vue3 官网对依赖注入解决的问题这样介绍的:

通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:

注意,虽然这里的 <Footer> 组件可能根本不关心这些 props,但为了使 <DeepChild> 能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为 “prop 逐级透传”,显然是我们希望尽量避免的情况。

provideinject 可以帮助我们解决这一问题。 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

这可以说就是 provide 和 inject 的来源,为了解决 Prop 逐级透传问题的问题

三、provide 和 inject 如何使用

这里部分也采用的官网的介绍,讲的很清楚

1.provide()

提供一个值,可以被后代组件注入。

  • 类型

    function provide<T>(key: InjectionKey<T> | string, value: T): void
    
    
    
  • 详细信息

    provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

    当使用 TypeScript 时,key 可以是一个被类型断言为 InjectionKey 的 symbol。InjectionKey 是一个 Vue 提供的工具类型,继承自 Symbol,可以用来同步 provide()inject() 之间值的类型。

    与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

  • 示例

    <script setup>
    import { ref, provide } from 'vue'
    import { fooSymbol } from './injectionSymbols'
    ​
    // 提供静态值
    provide('foo', 'bar')
    ​
    // 提供响应式的值
    const count = ref(0)
    provide('count', count)
    ​
    // 提供时将 Symbol 作为 key
    provide(fooSymbol, count)
    </script>
    
    
    

2.inject()

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

  • 类型

    // 没有默认值
    function inject<T>(key: InjectionKey<T> | string): T | undefined
    ​
    // 带有默认值
    function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
    ​
    // 使用工厂函数
    function inject<T>(
      key: InjectionKey<T> | string,
      defaultValue: () => T,
      treatDefaultAsFactory: true
    ): T
    
    
    
  • 详细信息

    第一个参数是注入的 key(这个 key 就是用来和 provide 设定的第一个参数进行匹配)。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会 “覆盖” 链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

    第二个参数是可选的,即在没有匹配到 key 时使用的默认值。

    第二个参数也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。在这种情况下,你必须将 true 作为第三个参数传入,表明这个函数将作为工厂函数使用,而非值本身。

    与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用。

    当使用 TypeScript 时,key 可以是一个类型为 InjectionKey 的 symbol。InjectionKey 是一个 Vue 提供的工具类型,继承自 Symbol,可以用来同步 provide()inject() 之间值的类型。

  • 示例

    假设有一个父组件已经提供了一些值,如前面 provide() 的例子中所示:

    <script setup>
    import { inject } from 'vue'
    import { fooSymbol } from './injectionSymbols'
    ​
    // 注入不含默认值的静态值
    const foo = inject('foo')
    ​
    // 注入响应式的值
    const count = inject('count')
    ​
    // 通过 Symbol 类型的 key 注入
    const foo2 = inject(fooSymbol)
    ​
    // 注入一个值,若为空则使用提供的默认值
    const bar = inject('foo', 'default value')
    ​
    // 注入一个值,若为空则使用提供的函数类型的默认值
    const fn = inject('function', () => {})
    ​
    // 注入一个值,若为空则使用提供的工厂函数
    const baz = inject('factory', () => new ExpensiveObject(), true)
    </script>
    
    
    

3. 和响应式数据配合使用

provide 和 inject 也可以传递响应式数据和方法,但是传递响应式数据的时候,官网做了一个推荐使用

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

如下面这个例子:

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
​
const location = ref('North Pole')
​
function updateLocation() {
  location.value = 'South Pole'
}
​
provide('location', {
  location,
  updateLocation
})
</script>


<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
​
const { location, updateLocation } = inject('location')
</script>
​
<template>
  <button @click="updateLocation">{{ location }}</button>
</template>


4. 使用 Symbol 作注入名

官网同样对注入名做了推荐,如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。可能平常很少用,这里对 symbol 做一下简单介绍。

symbol

在计算机编程中,Symbol 是一种基本数据类型,是在 ECMAScript 6 (ES6) 中引入的新特性。它是一种原始数据类型,与数字、字符串、布尔值等类似。

Symbol 是一种唯一且不可变的数据类型,每个 Symbol 值都是唯一的,不会与其他任何值相等,包括其他 Symbol 值。这使得 Symbol 非常适合用作对象属性的标识符,以确保不会发生属性名冲突。

创建 Symbol 可以使用全局 Symbol 函数,例如:

const mySymbol = Symbol();


也可以传递一个可选的描述字符串作为 Symbol 的标识,这个描述字符串对于调试和输出 Symbol 时是可选的,但并不影响 Symbol 的唯一性,例如:

const mySymbol = Symbol('my unique symbol');


应用到 provide 和 inject 中

通常推荐在一个单独的文件中导出这些注入名 Symbol:

// keys.js
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)


四、实现原理

翻了不少大佬的文章,发现有些理解不同,有说到利用到原型链的,有说没有用到的,仔细理了一下发现是因为 vue2 和 vue3 的组件之间的数据绑定和响应式系统不同导致的结果。

在 vue2 中组件实例方法和属性的继承是通过原型链来实现的,而provideinject 就是基于原型链的属性访问来实现跨组件通信。当一个组件通过 provide 提供数据,它会将这些数据添加到其原型链上,然后子组件通过 inject 可以在原型链上查找并访问这些数据。

在 Vue 3 中 组件的实例方法和属性的继承不再依赖于原型链,而是 引入了 Composition API,它采用了一种不同的方式来组织组件的代码和状态。组件的选项被重构为一个配置对象,其中 setup 函数用于定义组件的响应式数据、计算属性、方法等。这些选项不再依赖于原型链,而是直接导出给组件实例。

这个改进带来了以下好处:

  1. 更稳定的数据提供:在 Vue 3 中,每个组件实例都有自己的私有作用域,不会受到原型链的影响,因此不存在 Vue 2 中的潜在问题。
  2. 更好的类型检查:在 Vue 3 中,TypeScript 或 Flow 等类型检查工具可以更准确地检测到 inject 注入的数据类型。

因为平常大多数是在 Vue3 中使用,接下来详细介绍一下在 vue3 中的原理。

vue3 中的实现原理

provide 的原理

  • provide 是在父组件中使用的选项,用于提供数据给子组件。它实际上是一个函数,它会在父组件实例上创建一个名为 _provided 的对象。
  • _provided 对象存储了提供给子组件的数据,而且这些数据会在整个组件树中可用,子组件可以通过inject选项来访问这些数据。。
  • 当父组件提供的数据发生变化时,Vue 3 的响应式系统会追踪这些变化并通知所有依赖这些数据的子组件进行更新。

inject 的原理

  • 子组件通过inject选项声明需要注入的数据,可以是一个数组、一个对象或一个函数。这些声明告诉 Vue 3 要从父组件的提供数据中获取哪些属性。
  • 当子组件访问通过inject注入的数据时,Vue 3 会在组件树中向上搜索父组件,直到找到包含提供数据的组件或到达根组件。
  • 一旦找到包含提供数据的组件,Vue 3 会从该组件的_provided属性中获取相应的数据。
  • 如果提供的数据是响应式的,子组件将自动成为这些数据的依赖,当提供的数据发生变化时,子组件将被通知并进行更新。

五、优点和缺点

该方法可以说很方便解决了父组件给多级子组件传值的问题,但是同时也有一定的局限性,下面分析一下优点和缺点

优点:

  1. 解耦合(Decoupling): provideinject 有助于降低组件之间的耦合度。组件不需要直接了解其依赖项的实现细节,而是通过注入来访问这些依赖,这使得组件更加独立和可复用。
  2. 可测试性(Testability): 依赖注入使得单元测试更加容易。你可以轻松地注入模拟对象或测试替身,以测试组件的行为,而不需要实际的外部依赖。
  3. 可维护性(Maintainability): 通过将依赖项提取到外部并通过 provide 注入,代码变得更清晰和易于维护。这对于大型应用程序来说特别有价值。
  4. 可扩展性(Scalability): provideinject 可以用于共享全局配置、服务或状态管理等全局性的依赖项,使得应用程序更容易扩展和维护。

缺点:

  1. 复杂性(Complexity): 对于小型应用来说,使用 provideinject 可能会增加一些不必要的复杂性。这些特性最有价值的地方通常在大型和复杂的应用程序中。
  2. 容易滥用(Overuse): 有时开发人员可能会滥用 provideinject,将所有东西都注入到组件中,导致不必要的复杂性和混乱。需要谨慎权衡。
  3. 不适用于所有场景(Not Suitable for All Scenarios): provideinject 更适合用于共享全局配置和服务等情况,对于局部的、仅在某个组件内部使用的依赖项,使用 props 更合适。

六、总结

总的来说,provideinject 是一种强大的依赖注入机制,特别适用于大型和复杂的应用程序,以提高代码的可维护性、可测试性和可扩展性。但在小型应用中,可能会增加一些复杂性,需要谨慎使用。在使用时,需要根据具体的场景和需求来判断是否使用这些特性,在组件封装过程中,也可以根据情况进行使用。

往期更新

vue3 封装知识点(一)组件之前传值

vue3 封装知识点(二)v-model 的应用

参考资料

vue 官网