Provide
和Inject
可以在祖(父)组件和子(孙)组件间实现传值。相比prop
只能父子之间传值而言,Provide
和Inject
传值更方便,相比vuex
又更轻量。
接下来我们就从使用和实现原理两方面来介绍Provide
和Inject
。
Provide
和Inject
的使用
概括: 祖(父)组件中使用一个
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刷新。
Provide
和Inject
的原理分析
传值
- 组件实例对象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
}
根实例对象的
provides
是Object.create(null)
,也就是纯净的空对象{}
.当所有组件都没有使用
provide
函数时, 效果如下图所示:
- 在组件实例调用
provide
函数时,会将父组件的provides
为模板拷贝一份做为当前组件的provides
,不再指向父组件的provides
,然就将provide
中的key
和value
对应保存起来;
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
函数使用了相同的key
来提供数据,那他们的共同子(孙)组件用inject
函数获取到的数据value
是哪个值呢?
答案:很明显是离组件自身最近的(祖)父组件的
provide
提供的value
。
inject
函数的逻辑就非常简单了,就是取当前组件实例的provides
对象对应key
的value
, 如果没取到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。
Provide
和Inject
的缺陷
Provide
和Inject
进行传值的情况下,祖(父)组件 和 子(孙)组件间是相互独立的,也就是说祖(父)组件不关心是否有子(孙)组件使用其提供的数据,子(孙)组件 也不知道数据来自于哪个祖(父)组件。
数据的隔离了,但是会造成组件层级的高度耦合,例如子(孙)组件的正常运行必须要对应的祖(父)组件提供数据。否则就会功能异常。
所以Provide
和Inject
比较适合在功能库中使用(本来组件耦合度就很高的场景),而不是大型项目中。