1、前言顾名思义,爷孙组件是比 父子组件通信 要更深层次的引用关系(也有称之为 “隔代组件”):
c组件引入到b组件里,b组件引入到a组件里渲染,此时a是c的爷爷级别(可能还有更多层级关系),如果你用 props ,只能一级一级传递下去,那就太繁琐了,因此我们需要更直接的通信方式。
他们之间的关系如下,grandson.vue 并非直接挂载在 grandfather.vue 下面,他们之间还隔着至少一个 son.vue (可能有多个):
grandfather.vue└─son.vue └─grandson.vue
因为上下级的关系的一致性,爷孙组件通信的方案也适用于 父子组件通信 ,只需要把爷孙关系换成父子关系即可。
2、provide / inject这个特性有两个部分:grandfather.vue 有一个 provide 选项来提供数据,grandson.vue 有一个 inject 选项来开始使用这些数据。
grandfather.vue 通过 provide 向 grandson.vue 传值(可包含定义好的函数)
grandson.vue 通过 inject 向 grandfather.vue 触发爷爷组件的事件执行
无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者
这一部分的内容变化都特别大,但使用起来其实也很简单,不用慌,也有相同的地方:
父组件不需要知道哪些子组件使用它 provide 的 property
子组件不需要知道 inject property 来自哪里
另外,要切记一点就是:provide 和 inject 绑定并不是可响应的。这是刻意为之的,但如果传入了一个可监听的对象,那么其对象的 property 还是可响应的。
3、发起 provide我们先来回顾一下 2.x 的用法:
export default { // 定义好数据 data () { return { tags: [ '中餐', '粤菜', '烧腊' ] } }, // provide出去 provide () { return { tags: this.tags } }}
旧版的 provide 用法和 data 类似,都是配置为一个返回对象的函数。
3.x 的新版 provide, 和 2.x 的用法区别比较大。
在 3.x , provide 需要导入并在 setup 里启用,并且现在是一个全新的方法。
每次要 provide 一个数据的时候,就要单独调用一次。
每次调用的时候,都需要传入 2 个参数:
参数类型说明
key string 数据的名称
value any 数据的值
来看一下如何创建一个 provide:
// 记得导入provideimport { definecomponent, provide } from 'vue'export default definecomponent({ // ... setup () { // 定义好数据 const msg: string = 'hello world!'; // provide出去 provide('msg', msg); }})
操作非常简单对吧,但需要注意的是,provide 不是响应式的,如果你要使其具备响应性,你需要传入响应式数据
4、接收 inject也是先来回顾一下 2.x 的用法:
export default { inject: ['tags'], mounted () { console.log(this.tags); }}
旧版的 inject 用法和 props 类似,3.x 的新版 inject, 和 2.x 的用法区别也是比较大。
在 3.x, inject 和 provide 一样,也是需要先导入然后在 setup 里启用,也是一个全新的方法。
每次要 inject 一个数据的时候,就要单独调用一次。
每次调用的时候,只需要传入 1 个参数:
参数类型说明
key string 与provide对应的数据名称
// 记得导入injectimport { definecomponent, inject } from 'vue'export default definecomponent({ // ... setup () { const msg: string = inject('msg') || ''; }})
也是很简单(写 ts 的话,由于 inject 到的值可能是 undefined,所以要么加个 undefined 类型,要么给变量设置一个空的默认值)。
5、响应性数据的传递与接收之所以要单独拿出来说, 是因为变化真的很大
在前面我们已经知道,provide 和 inject 本身不可响应,但是并非完全不能够拿到响应的结果,只需要我们传入的数据具备响应性,它依然能够提供响应支持。
我们以 ref 和 reactive 为例,来看看应该怎么发起 provide 和接收 inject。
先在 grandfather.vue 里 provide 数据:
export default definecomponent({ // ... setup () { // provide一个ref const msg = ref<string>('hello world!'); provide('msg', msg); // provide一个reactive const userinfo: member = reactive({ id: 1, name: 'petter' }); provide('userinfo', userinfo); // 2s 后更新数据 settimeout(() => { // 修改消息内容 msg.value = 'hi world!'; // 修改用户名 userinfo.name = 'tom'; }, 2000); }})
在 grandsun.vue 里 inject 拿到数据:
export default definecomponent({ setup () { // 获取数据 const msg = inject('msg'); const userinfo = inject('userinfo'); // 打印刚刚拿到的数据 console.log(msg); console.log(userinfo); // 因为 2s 后数据会变,我们 3s 后再看下,可以争取拿到新的数据 settimeout(() => { console.log(msg); console.log(userinfo); }, 3000); // 响应式数据还可以直接给 template 使用,会实时更新 return { msg, userinfo } }})
非常简单,非常方便!!!
响应式的数据 provide 出去,在子孙组件拿到的也是响应式的,并且可以如同自身定义的响应式变量一样,直接 return 给 template 使用,一旦数据有变化,视图也会立即更新。
但上面这句话有效的前提是,不破坏数据的响应性,比如 ref 变量,你需要完整的传入,而不能只传入它的 value,对于 reactive 也是同理,不能直接解构去破坏原本的响应性。
切记!切记!!!
6、引用类型的传递与接收 (针对非响应性数据的处理)provide 和 inject 并不是可响应的,这是官方的故意设计,但是由于引用类型的特殊性,在子孙组件拿到了数据之后,他们的属性还是可以正常的响应变化。
先在 grandfather.vue 里 provide 数据:
export default definecomponent({ // ... setup () { // provide 一个数组 const tags: string[] = [ '中餐', '粤菜', '烧腊' ]; provide('tags', tags); // provide 一个对象 const userinfo: member = { id: 1, name: 'petter' }; provide('userinfo', userinfo); // 2s 后更新数据 settimeout(() => { // 增加tags的长度 tags.push('叉烧'); // 修改userinfo的属性值 userinfo.name = 'tom'; }, 2000); }})
在 grandsun.vue 里 inject 拿到数据:
export default definecomponent({ setup () { // 获取数据 const tags: string[] = inject('tags') || []; const userinfo: member = inject('userinfo') || { id: 0, name: '' }; // 打印刚刚拿到的数据 console.log(tags); console.log(tags.length); console.log(userinfo); // 因为 2s 后数据会变,我们 3s 后再看下,能够看到已经是更新后的数据了 settimeout(() => { console.log(tags); console.log(tags.length); console.log(userinfo); }, 3000); }})export default definecomponent({ setup () { // 获取数据 const tags: string[] = inject('tags') || []; const userinfo: member = inject('userinfo') || { id: 0, name: '' }; // 打印刚刚拿到的数据 console.log(tags); console.log(tags.length); console.log(userinfo); // 因为 2s 后数据会变,我们 3s 后再看下,能够看到已经是更新后的数据了 settimeout(() => { console.log(tags); console.log(tags.length); console.log(userinfo); }, 3000); }})
引用类型的数据,拿到后可以直接用,属性的值更新后,子孙组件也会被更新。
但是!!!由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用 响应式 api 。
7、基本类型的传递与接收 (针对非响应性数据的处理)基本数据类型被直接 provide 出去后,再怎么修改,都无法更新下去,子孙组件拿到的永远是第一次的那个值。
先在 grandfather.vue 里 provide 数据:
export default definecomponent({ // ... setup () { // provide 一个数组的长度 const tags: string[] = [ '中餐', '粤菜', '烧腊' ]; provide('tagscount', tags.length); // provide 一个字符串 let name: string = 'petter'; provide('name', name); // 2s 后更新数据 settimeout(() => { // tagscount 在 grandson 那边依然是 3 tags.push('叉烧'); // name 在 grandson 那边依然是 petter name = 'tom'; }, 2000); }})
在 grandsun.vue 里 inject 拿到数据:
export default definecomponent({ setup () { // 获取数据 const name: string = inject('name') || ''; const tagscount: number = inject('tagscount') || 0; // 打印刚刚拿到的数据 console.log(name); console.log(tagscount); // 因为 2s 后数据会变,我们 3s 后再看下 settimeout(() => { // 依然是 petter console.log(name); // 依然是 3 console.log(tagscount); }, 3000); }})
很失望,并没有变化。
那么是否一定要定义成响应式数据或者引用类型数据呢?
当然不是,我们在 provide 的时候,也可以稍作修改,让它能够同步更新下去。
先在 grandfather.vue 里 provide 数据:
export default definecomponent({ // ... setup () { // provide 一个数组的长度 const tags: string[] = [ '中餐', '粤菜', '烧腊' ]; provide('tagscount', (): number => { return tags.length; }); // provide 字符串 let name: string = 'petter'; provide('name', (): string => { return name; }); // 2s 后更新数据 settimeout(() => { // tagscount 现在可以正常拿到 4 了 tags.push('叉烧'); // name 现在可以正常拿到 tom 了 name = 'tom'; }, 2000); }})
再来 grandsun.vue 里修改一下 inject 的方式,看看这次拿到的数据:
export default definecomponent({ setup () { // 获取数据 const tagscount: any = inject('tagscount'); const name: any = inject('name'); // 打印刚刚拿到的数据 console.log(tagscount()); console.log(name()); // 因为 2s 后数据会变,我们 3s 后再看下 settimeout(() => { // 现在可以正确得到 4 console.log(tagscount()); // 现在可以正确得到 tom console.log(name()); }, 3000); }})
这次可以正确拿到数据了,看出这2次的写法有什么区别了吗?
基本数据类型,需要 provide 一个函数,将其 return 出去给子孙组件用,这样子孙组件每次拿到的数据才会是新的。
但由于不具备响应性,所以子孙组件每次都需要重新通过执行 inject 得到的函数才能拿到最新的数据。
按我个人习惯来说,使用起来挺别扭的,能不用就不用……
由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用 响应式 api 。
以上就是vue3全局组件通信之provide/inject源码分析的详细内容。