Vue 3.0 Provide和Inject实现共享数据

4,656 阅读5分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

ProvideInject可以在祖(父)组件和子(孙)组件间实现传值。相比prop只能父子之间传值而言,ProvideInject传值更方便,相比vuex又更轻量。

接下来我们就从使用和实现原理两方面来介绍ProvideInject

ProvideInject的使用

概括: 祖(父)组件中使用一个provide函数来提供数据,而子(孙)组件使用inject函数来获取数据。

provide API 的使用

我们这里就用官方的例子, 阅读过官方例子的童鞋可以跳过本节。

  • 使用前需要先从vue中引用provide函数
import { provide } from "vue";

使用插件后,在敲完provide代码后编辑器(例如VS Code)可以自动引入这个函数,不需要手动引入。这里只是为了说明。

  • setup函数中调用provide函数提供数据
<!-- GrandParent.vue -->
setup() {
    // 省略其他...
    provide('location', '北极')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
}

provide有两个参数:第一个参数表示字符串类型的key;第二个参数为value,,可以是对象,也可以是普通数据类型.

inject API 的使用
  • 使用前需要先从vue中引用inject函数
import { inject } from "vue";
  • setup函数中调用inject函数获取数据
<!-- GrandSon.vue -->
setup() {
    // 省略其他...
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    
  },

inject也有两个参数:第一个参数表示需要获取的key;第二个参数为key未取到值时候填充的的默认值(非必选参数)。如果没有设置默认值,则没有取到值则为undefined

provide添加响应性

添加响应性是指provide提供的数据发生变化时,inject使用数据的组件需要进行刷新界面。我们想到肯定得需要Vue3.0的一些响应式的API

  • 修改后的provide样子如下:
<!-- GrandParent.vue -->
setup() {
    // 省略其他...
    provide('location', ref('北极'))
    provide('geolocation', reactive({
      longitude: 90,
      latitude: 135
    }))
}
  • inject的使用没有差别,但是如果使用inject的组件模板中用到provide提供的数据,则组件会及时进行UI刷新。

ProvideInject的原理分析

传值

  • 组件实例对象ComponentInternalInstance有一个provides属性;
<!-- components.ts -->
export interface ComponentInternalInstance {
  **
   * Object containing values this component provides for its descendents
   * @internal
   */
  provides: Data
}
  • 在创建组件实例对象时候,provides属性会指向父组件实例对象的provides属性;
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {

  const instance: ComponentInternalInstance = {
    // 省略其他属性...
    provides: parent ? parent.provides : Object.create(appContext.provides),
  }
  
  return instance
}

根实例对象的providesObject.create(null),也就是纯净的空对象{}.

当所有组件都没有使用provide函数时, 效果如下图所示:

provide

  • 在组件实例调用provide函数时,会将父组件的provides为模板拷贝一份做为当前组件的provides,不再指向父组件的provides,然就将provide中的keyvalue对应保存起来;
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  let provides = currentInstance.provides
  const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
  // 如果指向父组件的`provides`对象则拷贝一份
  if (parentProvides === provides) {
    provides = currentInstance.provides = Object.create(parentProvides)
  }
  // 将`provide`的`key`和`value`对应保存起来
  provides[key as string] = value
}

当某个组件有使用provide函数时, 效果如下图所示:

provide

思考题: 如果(祖)父和子(孙)组件provide函数使用了相同的key来提供数据,那他们的共同子(孙)组件用inject函数获取到的数据value是哪个值呢?

答案:很明显是离组件自身最近的(祖)父组件的provide提供的value

  • inject函数的逻辑就非常简单了,就是取当前组件实例的provides对象对应keyvalue, 如果没取到value就用默认值defaultValue,如果没有默认值defaultValue结果就是undefined;
function inject(key, defaultValue, treatDefaultAsFactory = false) {
  const instance = currentInstance || currentRenderingInstance;
  if (instance) {
      // 取值
      const provides = instance.parent == null
          ? instance.vnode.appContext && instance.vnode.appContext.provides
          : instance.parent.provides;
      if (provides && key in provides) {
          return provides[key];
      }
      else if (arguments.length > 1) {
          return treatDefaultAsFactory && isFunction(defaultValue)
              ? defaultValue.call(instance.proxy)
              : defaultValue;
      }
  }
}

响应式

这个逻辑其实不用过多分析了,和在本组件内申明一个响应式数据是没有差别的,可参考前面的文章。响应式数据变化后会触发组件的副作用渲染函数更新UI。

ProvideInject的缺陷

ProvideInject进行传值的情况下,祖(父)组件子(孙)组件间是相互独立的,也就是说祖(父)组件不关心是否有子(孙)组件使用其提供的数据,子(孙)组件 也不知道数据来自于哪个祖(父)组件

数据的隔离了,但是会造成组件层级的高度耦合,例如子(孙)组件的正常运行必须要对应的祖(父)组件提供数据。否则就会功能异常。

所以ProvideInject 比较适合在功能库中使用(本来组件耦合度就很高的场景),而不是大型项目中。