各位观众,晚上好!我是你们今晚的Vue 3源码导读小助手,今天咱们就来聊聊Vue 3里边那个有点“神秘”但又特别实用的 provide
和 inject
。
咱们先来定个基调,provide
和 inject
就像是组件树里的“广播台”和“收音机”。祖先组件通过 provide
广播一些信息(数据,方法啥的),后代组件通过 inject
接收这些信息,实现跨层级组件通信,而且还能保持响应式!是不是有点意思?
一、provide
:我是祖宗,我发福利!
首先,咱们来看看 provide
是怎么工作的。简单来说,provide
就是在组件实例上注册一些数据,供后代组件使用。
1.1 provide
的两种用法
Vue 3 提供了两种 provide
的写法:
-
对象形式: 简单粗暴,直接提供一个对象。
// 父组件 import { provide, ref } from 'vue'; export default { setup() { const message = ref('Hello from parent!'); provide('message', message); // 提供一个名为 'message' 的响应式数据 provide('author', '张三'); //提供一个名为‘author’的普通数据 return {}; } }
-
函数形式: 灵活多变,可以根据需要动态提供数据。
// 父组件 import { provide, ref } from 'vue'; export default { setup() { const count = ref(0); provide('getCount', () => count.value); // 提供一个获取 count 值的函数 //每隔一秒count加1 setInterval(() => { count.value++ }, 1000); return {}; } }
1.2 源码剖析:provide
到底干了啥?
provide
的核心逻辑其实隐藏在组件的 setup
函数执行过程中。 当我们使用 provide
时,实际上是在当前组件实例的 provides
属性上存储数据。注意,这里说的组件实例是指 VNode 对应的组件实例,而不是简单的 JavaScript 对象。
让我们简化一下源码(为了方便理解,省去了一些边界条件和性能优化):
// 简化版 provide 实现
function provide(key, value) {
if (!currentInstance) {
//在 setup 函数之外调用 provide,会报错。
console.warn(`provide() can only be used inside setup().`);
return;
}
let provides = currentInstance.provides; // 获取当前组件实例的 provides 属性
// 如果 provides 属性和父组件的 provides 属性相等,说明是第一次 provide
// 此时,需要创建一个原型指向父组件 provides 的对象,防止修改子组件的 provide 影响到父组件
if (provides === currentInstance.parent?.provides) {
provides = currentInstance.provides = Object.create(provides);
}
provides[key] = value; // 将 key-value 存储到 provides 对象中
}
几个关键点:
currentInstance
:指向当前正在执行setup
函数的组件实例。Vue 3 通过一个全局变量来维护当前组件实例,有点像一个临时的“上下文”。provides
:每个组件实例都有一个provides
属性,它是一个对象,用于存储provide
提供的键值对。- 原型链大法: 如果当前组件是第一次
provide
,那么 Vue 会创建一个新的provides
对象,并将它的原型指向父组件的provides
对象。这样做的目的是为了实现“继承”,并且隔离父子组件的provide
。也就是说,子组件修改自己provides
里的数据,不会影响到父组件的provides
。 如果不这样做,那么子组件直接修改父组件的provides
对象,会导致父组件的状态也发生改变,这显然是不合理的。
二、inject
:我是孙子,我来领福利!
现在,咱们来看看 inject
是怎么接收 provide
提供的福利的。
2.1 inject
的用法
inject
允许后代组件接收祖先组件通过 provide
提供的数据。
// 子组件
import { inject, onMounted } from 'vue';
export default {
setup() {
const message = inject('message', 'Default message'); // 接收名为 'message' 的数据,如果没有提供,则使用默认值 'Default message'
const getCount = inject('getCount'); // 接收名为 'getCount' 的函数
onMounted(() => {
if (getCount) {
console.log('Count:', getCount());
}
});
return {
message,
};
}
}
inject
接收两个参数:
key
:要接收的数据的键名,和provide
时的键名对应。defaultValue
(可选):如果祖先组件没有提供对应的数据,则使用默认值。
2.2 源码剖析:inject
是怎么找到福利的?
inject
的实现稍微复杂一些,因为它需要在组件树中向上查找 provide
提供的数据。
简化版源码:
// 简化版 inject 实现
function inject(key, defaultValue) {
if (!currentInstance) {
console.warn(`inject() can only be used inside setup().`);
return;
}
// 从父组件开始,沿着原型链向上查找 provides
let provides = currentInstance.parent?.provides;
while (provides && !(key in provides)) {
provides = Object.getPrototypeOf(provides);
}
if (provides && key in provides) {
return provides[key]; // 找到了,返回对应的值
} else if (arguments.length > 1) {
return defaultValue; // 没找到,返回默认值
} else {
console.warn(`Injection "${key}" not found`);
}
}
关键点:
- 原型链查找:
inject
从当前组件的父组件开始,沿着原型链向上查找provides
对象,直到找到包含指定key
的provides
对象为止。 - 默认值: 如果在整个组件树中都没有找到对应的
key
,并且提供了defaultValue
,则返回defaultValue
。 - 警告: 如果没有找到对应的
key
,也没有提供defaultValue
,则会发出警告。
三、响应式传递:福利不能过期!
provide
和 inject
最厉害的地方在于,它能够保持数据的响应式。也就是说,如果 provide
提供的数据是响应式的(例如,通过 ref
或 reactive
创建),那么 inject
接收到的数据也会是响应式的。
3.1 响应式原理
Vue 3 的响应式系统基于 Proxy 实现。当我们通过 ref
或 reactive
创建响应式数据时,Vue 会对数据进行“代理”,拦截数据的读取和修改操作,并在数据发生变化时通知相关的组件进行更新。
当 provide
提供响应式数据时,实际上是将 Proxy 对象存储到 provides
对象中。inject
接收到的也是这个 Proxy 对象。因此,当响应式数据发生变化时,inject
接收数据的组件也会收到通知,并进行更新。
3.2 示例
// 父组件
import { provide, ref } from 'vue';
export default {
setup() {
const count = ref(0);
provide('count', count);
setInterval(() => {
count.value++;
}, 1000);
return {};
}
};
// 子组件
import { inject } from 'vue';
export default {
setup() {
const count = inject('count');
return {
count,
};
},
template: `<div>Count: {{ count }}</div>`
};
在这个例子中,父组件通过 provide
提供了响应式数据 count
。子组件通过 inject
接收到了 count
。当父组件的 count
值发生变化时,子组件的视图也会自动更新。
四、provide/inject
的使用场景
provide/inject
主要用于以下场景:
- 跨层级组件通信: 当需要将数据从祖先组件传递给后代组件,但中间组件不需要使用这些数据时,可以使用
provide/inject
。避免了逐层传递 props 的麻烦。 - 主题配置: 可以使用
provide/inject
在根组件中提供主题配置,然后在后代组件中使用这些配置。 - 插件开发: 插件可以使用
provide/inject
向组件树中注入一些功能或服务。
五、provide/inject
的注意事项
- 非响应式数据: 如果
provide
提供的是非响应式数据(例如,普通字符串、数字等),那么inject
接收到的数据也是非响应式的。 - 依赖注入:
provide/inject
本质上是一种依赖注入的方式。过度使用依赖注入可能会导致代码难以维护和调试。 - Symbol 作为 Key: 可以使用 Symbol 作为
provide
和inject
的 key,以避免命名冲突。
六、深入源码:createApp
和 rootContext
的关联
createApp
是 Vue 3 中创建应用程序的入口函数。它会创建一个应用程序实例,并将根组件挂载到指定的 DOM 元素上。
在 createApp
的过程中,Vue 会创建一个 rootContext
对象,并将它作为根组件的 provides
属性。这意味着,所有组件都可以通过 inject
访问 rootContext
中存储的数据。
这个机制为插件开发提供了一种便捷的方式。插件可以通过修改 rootContext
,向整个应用程序注入一些全局的功能或服务。
七、总结
provide
和 inject
是 Vue 3 中一个强大的跨层级组件通信机制。它允许祖先组件向后代组件提供数据,并且能够保持数据的响应式。
特性 | provide |
inject |
---|---|---|
作用 | 祖先组件提供数据给后代组件 | 后代组件接收祖先组件提供的数据 |
使用方式 | 对象形式或函数形式 | inject(key, defaultValue) |
查找方式 | N/A | 沿着组件树向上查找 provides 对象,直到找到包含指定 key 的 provides 对象为止 |
响应式 | 如果提供的数据是响应式的,那么接收到的数据也是响应式的 | 如果祖先组件提供的数据是响应式的,那么接收到的数据也是响应式的 |
适用场景 | 跨层级组件通信、主题配置、插件开发 | 跨层级组件通信、主题配置、插件开发 |
注意事项 | 可以使用 Symbol 作为 key,以避免命名冲突。如果 provide 提供的是非响应式数据,那么 inject 接收到的数据也是非响应式的。过度使用依赖注入可能会导致代码难以维护和调试。 | 可以使用 Symbol 作为 key,以避免命名冲突。如果祖先组件提供的是非响应式数据,那么接收到的数据也是非响应式的。过度使用依赖注入可能会导致代码难以维护和调试。如果没有找到对应的 key,也没有提供 defaultValue,则会发出警告。 |
源码实现 | 存储到组件实例的 provides 属性中 |
从当前组件的父组件开始,沿着原型链向上查找 provides 对象 |
与 createApp 的关联 | 通过 createApp 创建应用程序时,Vue 会创建一个 rootContext 对象,并将它作为根组件的 provides 属性。这意味着,所有组件都可以通过 inject 访问 rootContext 中存储的数据 |
通过 createApp 创建应用程序时,Vue 会创建一个 rootContext 对象,并将它作为根组件的 provides 属性。这意味着,所有组件都可以通过 inject 访问 rootContext 中存储的数据 |
希望今天的讲解能够帮助大家更好地理解 Vue 3 中 provide
和 inject
的底层实现。 记住,理解源码才能更好地使用框架,写出更优雅的代码!
感谢大家的收看,咱们下期再见!