provide 和 inject的原理以及使用范围

279 阅读9分钟

1. provideinject 的基本工作原理

  • provide:在祖先组件中声明的 provide 是用来“提供”数据给后代组件的。它可以是一个对象或返回对象的函数。
  • inject:在后代组件中声明的 inject 用来“注入”祖先组件通过 provide 提供的数据。

Vue.js 的 provideinject 通过 Vue 的组件上下文来传递数据。也就是说,provide 所提供的数据会存储在祖先组件的上下文中,而后代组件在初始化时,会从组件树中向上查找 provide 所提供的数据,然后注入到自身的作用域中。

2. 具体的实现步骤

2.1 provide 的实现

provide 是在组件实例创建的时候执行的。它的实现原理可以简化为以下步骤:

  1. 声明 provide

    • 祖先组件在创建时,如果定义了 provide,它的内容会被存储到这个组件的上下文中。
    • provide 可以是一个对象或返回对象的函数。
  2. 数据的存储

    • Vue 会将 provide 的数据存储在组件实例的 _provided 属性中,作为一个对象存储。
2.2 inject 的实现

inject 的实现主要是依赖组件的继承上下文机制。在组件的创建过程中:

  1. 声明 inject

    • 后代组件声明 inject,这可以是一个字符串数组或者对象。Vue 在创建组件时,会根据 inject 的定义查找祖先组件中通过 provide 提供的相应数据。
  2. 查找 provide 的数据

    • Vue 会在组件的初始化阶段,从当前组件的父级组件开始,向上查找其祖先组件,找到包含对应 provide 的组件,并将这个数据注入到后代组件的 this 中。
    • 具体来说,inject 会依次检查父组件的 _provided,找到对应的数据。
  3. 数据的注入

    • 如果找到 provide 的数据,它将被注入到后代组件的 this 中,后代组件就可以直接通过 this.someValue 来使用这个数据。

3. 详细源码分析

下面我们通过 Vue 的源码来进一步了解 provideinject 的实现原理。

provide 的实现

在 Vue 中,provide 的逻辑实现很简单,主要是在组件实例化时,将 provide 的内容存储在组件实例的 _provided 属性中。

 // Vue 源码片段:初始化 provide
 export function initProvide(vm: Component) {
   const provide = vm.$options.provide;
   if (provide) {
     vm._provided = typeof provide === 'function'
       ? provide.call(vm) // 如果是函数,则执行该函数
       : provide;         // 否则直接将对象存储
   }
 }
  • initProvide 函数会在组件初始化时执行,它会检查当前组件是否有 provide 选项。
  • 如果 provide 是函数,就执行这个函数,并将其返回值(一个对象)存储在组件实例的 _provided 属性中。如果 provide 是对象,就直接将它存储。
inject 的实现

在组件初始化时,Vue 会调用 resolveInject 函数来处理 inject 的注入过程。

 // Vue 源码片段:解析 inject
 export function resolveInject(inject: any, vm: Component): ?Object {
   if (inject) {
     const result = Object.create(null);
     const keys = hasSymbol
       ? Reflect.ownKeys(inject)
       : Object.keys(inject);
 ​
     for (let i = 0; i < keys.length; i++) {
       const key = keys[i];
       const provideKey = inject[key].from;
       let source = vm;
       while (source) {
         if (source._provided && hasOwn(source._provided, provideKey)) {
           result[key] = source._provided[provideKey];
           break;
         }
         source = source.$parent;  // 向上查找父组件,直到找到注入的 provide
       }
     }
     return result;
   }
 }

resolveInject 的核心逻辑是:

  1. 遍历 inject 的 keys:获取需要注入的属性(可以是字符串数组或对象)。
  2. 向上查找 provide:从当前组件的实例开始,逐级向上查找父组件的 _provided,直到找到包含 provide 数据的组件。
  3. 注入数据:找到 provide 的数据后,将其注入到当前组件中。

如果没有找到对应的 provide 数据,Vue 并不会报错。也可以通过为 inject 提供默认值来避免数据未找到的情况。

4. 响应式处理

在 Vue 3 中,provideinject 传递的默认数据不是响应式的。如果需要让提供的数据在后代组件中具备响应性,可以使用 Vue 3 的响应式 API,如 reactiveref,让数据具备响应性。

响应式 provide 的示例
 import { reactive, provide, inject } from 'vue';
 ​
 // 父组件中提供响应式数据
 export default {
   setup() {
     const state = reactive({
       message: 'Hello from parent'
     });
 ​
     provide('state', state);
   }
 };
 ​
 // 子组件中注入响应式数据
 export default {
   setup() {
     const state = inject('state');
     return {
       state
     };
   }
 };
  • 在这个例子中,父组件通过 provide 提供了一个响应式对象 state
  • 子组件通过 inject 获取 state,并且可以享受到 Vue 的响应式更新机制。

是的,Vue 官方确实建议将 provideinject 主要用于高阶插件组件库开发,而不建议在普通应用程序中广泛使用它们。这是因为 provideinject 设计初衷和适用场景的特殊性,使得它更适合在特定情况下使用。以下是官方建议及原因:

5.为什么 provideinject 不推荐用于普通应用程序代码

  1. 隐式依赖关系

    • provideinject 会使得组件之间的依赖关系变得隐式,无法通过组件的 props 或事件明确体现。这种隐式的依赖关系在大型应用程序中可能会导致代码的可读性和可维护性下降,因为子组件的依赖无法在其定义中直观地看到。
    • 使用 props 和事件通信可以更加明确地表达组件之间的关系,而 provideinject 是通过组件层级结构注入的,减少了直接的上下文可见性。
  2. 调试困难

    • 由于 inject 依赖于 provide,如果后代组件未能成功接收到 provide 的数据,问题的定位可能比较困难。尤其在复杂的应用中,查找某个组件从哪一个祖先组件获得了 provide 的数据,或者找出某个层级的 provide 出现了问题,可能会造成调试上的困扰。
    • 相比之下,propsemit 的父子关系是明确的,调试时可以很容易地跟踪数据流动。
  3. 不利于维护

    • 在普通应用程序中,层级之间的数据传递通常通过 props 或 Vuex 等状态管理工具实现,这使得数据流更加直观和统一。provideinject 的使用可能导致数据流的追踪复杂化,尤其是在组件层级嵌套较深时,难以理解数据的来源和流向。
    • 在较为复杂的应用中,过度使用 provideinject 可能会使维护变得困难,尤其是当多个层级之间需要共享数据时,可能会造成依赖混乱。
  4. 有限的双向数据绑定支持

    • provideinject 的机制主要是从祖先组件后代组件传递数据,但后代组件不能直接修改 provide 的数据。如果需要在后代组件中更新数据,通常还是需要使用 $emit 来触发事件,或者依赖 Vuex 等其他状态管理工具进行全局状态的同步。
    • 对于普通的应用程序,父子组件间的数据传递与通信,使用 props$emit 已经足够清晰且符合 Vue 的单向数据流设计原则。

6.为什么 provideinject 适合高阶插件/组件库?

  1. 跨层级状态共享

    • 在插件或组件库开发中,某些全局状态(如主题、国际化、全局配置)需要在多个不相关的组件之间共享。这种情况下,provideinject 可以避免通过逐级传递 props 来传播数据,简化了状态共享的实现过程。
    • 比如,在开发一个国际化插件时,provide 可以用来为所有嵌套在应用中的组件提供国际化信息,而组件本身不需要关心这个状态的来源。
  2. 灵活性和可扩展性

    • 高阶插件或组件库通常需要提供一种灵活的机制,让开发者可以在不同的上下文中使用组件。通过 provideinject,插件开发者可以为组件库中的各个组件提供必要的上下文支持,而组件库使用者不需要手动管理这些上下文状态。
    • 例如,开发一个 UI 组件库时,provide 可以为所有子组件提供全局主题或配置,子组件只需使用 inject 来获取所需的数据,而不必显式声明每个组件都依赖这些配置。
  3. 解耦合设计

    • provideinject 是一种典型的依赖注入模式(Dependency Injection),它能够有效地解耦组件间的依赖关系。在开发高阶组件或插件时,这种模式有助于降低组件间的耦合度,提高模块的复用性和灵活性。
    • 例如,开发一个表单组件库,父级组件可以通过 provide 提供表单的上下文状态(如验证信息、数据模型等),而每个表单控件组件可以通过 inject 获取这些上下文信息,简化了组件之间的依赖关系。
  4. 封装复杂逻辑

    • provideinject 非常适合在插件中封装复杂的内部实现细节,而不暴露给外部使用者。例如,开发一个复杂的表单管理库,可以通过 provide 提供表单状态管理对象,子组件(如表单字段)可以通过 inject 访问和操作表单数据,外部开发者不需要知道这些内部实现细节。

适用场景对比

特性普通应用程序代码高阶插件/组件库
数据流动倾向于使用 props 和事件倾向于共享全局状态、上下文
依赖管理依赖显式的父子组件传递依赖注入,跨层级传递更灵活
调试和可维护性调试更直观插件开发时避免逐级传递
可读性父子关系明确,数据流清晰解耦合设计,依赖更灵活
应用场景普通组件间的通信跨层级的数据共享、插件开发

何时使用 provideinject

  • 不适合使用的场景

    • 在普通的应用开发中,不建议滥用 provideinject,因为它们使得组件间的数据流不够清晰,增加了代码复杂性。在这些场景中,使用 props$emit 进行组件间的数据通信更为合适。
  • 适合使用的场景

    • 当需要开发高阶组件插件库,并且希望在多个层级的组件之间共享一些全局状态时,provideinject 可以提供非常简洁且高效的解决方案。

总结

  • provide 的原理:在祖先组件实例化时,provide 的数据会被存储在组件实例的 _provided 属性中,用于后代组件获取。
  • inject 的原理:在后代组件初始化时,Vue 会从组件树向上查找 provide 的数据,并将其注入到后代组件中,供组件使用。
  • 跨层级通信provideinject 可以实现跨层级的数据传递,避免了逐层传递 props,在开发高阶组件或组件库时非常有用。
  • 响应式数据:默认情况下,provideinject 传递的数据不是响应式的,但在 Vue 3 中可以通过 reactiveref 使其具备响应式。