Vue 3.0 Provide和Inject实现共享数据

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 比较适合在功能库中使用(本来组件耦合度就很高的场景),而不是大型项目中。

猜你喜欢

转载自juejin.im/post/7053040504220418062