Vue3.x 从零开始(四)—— 更完善的组件传参
在如今的前端开发工作中, 组件之间的参数传递是一个非常常见的问题
Vue 2 已经有了一套非常实用的组件传参机制,Vue 3 在原本的基础上做了些改进
一、父组件传参到子组件
在《Vue3.x 从零开始(二)—— 重新认识 Vue 组件》中已经介绍过 Props
这是最常用的父对子传参方式
上面演示的参数只是简单的字符串,也可以通过 v-bind 指令传入 Number、Function、Object 等类型
除了传递基本的参数之外,props 还可以用来传递组件
由于组件是变量的形式传入,所以在子组件中需要动态渲染组件,这就会用到内置组件 <component />
<component /> 组件会接收 is 参数,并基于这个参数渲染组件,所以我们只要通过 is 属性传入组件变量即可
子组件定义 prop:
父组件传入组件:
在父组件部分,我们需要把 Alert 组件传给子组件,所以需要引入组件之后,将 Alert 组件保存到 data
data 是具有响应性的,而组件作为变量传递的时候,是不需要具备响应性的
所以我使用了 markRaw 方法来消除响应性( markRaw 会标记一个对象,使其永远不会转换为代理,只返回对象本身)
而在 Vue 2.x 中并没有类似的方法,会对整个组件做数据劫持,导致额外的性能开销
在写文的时候有同事提出了一个问题:prop 传入组件和 slot 有什么区别?
slot 是在父组件中插入组件,被插入的内容和子组件无关,所有逻辑都在父组件中完成
而 prop 传入的组件恰恰相反,父组件只是决定传入的内容,相关逻辑是在子组件中完成
二、子组件传参到父组件
子组件通常使用自定义事件的方式向父组件传参,即 $emit
this.$emit('event-name', data);
$emit 函数接收的第一个参数是自定义的事件名称,这个事件名称建议采用全小写的 kebab-case 事件名
除了事件名称外,$emit 还可以接收数量不定的额外参数,这些额外参数会按顺序传递给父组件的事件处理函数
来看一下自定义事件的完整用法,首先在子组件中通过 $emit 定义事件和需要传递的参数
然后在父组件中监听该事件,并通过事件处理函数接收子组件的传参
通过 $emit 我们还可以做一些更厉害的事情,比如对 prop 实现双向绑定
prop 是单向下行绑定,也就是说,父级 prop 的更新会在子组件中响应,而子组件无法修改 prop 的值
如果子组件确实需要修改 prop 的值,需要使用 update:prop 事件
比如有一个 prop 的属性名为 text,我们可以在子组件中通过 $emit 触发 update:text 事件并传参,然后在父组件进行赋值
Vue 3 为了提升开发者的效率,还提供了 v-model 语法。在父组件中使用 v-model:prop ,就不需要监听 update 事件了
// Vue 2 中的 .sync 修饰符已移除
但子组件还是要用 $emit 触发 update 事件 this.$emit('update:text', data);
如果是 Vue 2 的用户,应该已经发现 Vue 3 中 v-model 的用法已经发生了改变
在 Vue 2 中,在组件上使用 v-model 相当于绑定了一个属性名为 value 的 prop,并监听了 input 事件
而在 Vue 3 中,v-model 相当于传递了一个名为 modelValue 的 prop,并监听 update:modelValue 事件
<child-component v-model="text" /> <!-- 在 Vue 3 中,这两种写法是等价的 --> <child-component :modelValue="text" @update:modelValue="text = $event" />
也就是说,Vue 3 中的 v-model 是 prop + update 的语法糖,只是当 prop 被定义为 modelValue 的时候可以省略 :modelValue
三、深层次的组件传参
上面介绍是父子组件之间的数据传递,而对于深嵌套的祖孙组件:
App.vue └─ Home.vue └─ Footer.vue ├─ FooterItem.vue └─ FooterTips.vue
像这样的结构,如果 Home 要传递一个参数给 FooterTips,继续使用 prop 或者 $emit 就会很复杂
// 在 Vue 2 中可以使用 event bus 来处理,但 Vue 3 中移除了 $on、$off,所以已经无法构建 event bus 了
这时候可以使用 provide / inject
首先在孙组件 footer-tips 中通过 inject 定义需要传入的参数,inject 可以是一个由属性名组成的字符串数组
然后在祖父级组件中通过 provide 传入对应的参数
provide 也可以是一个对象,但为了更安全的开发组件,建议始终将 provide 定义为返回对象的函数
如果需要定义 inject 的默认值,也可以像 props 一样,将 inject 定义为对象:
inject: { author: { default: '这是一个沉默的作者', }, // 如果是 Object 或者 Array 这种引用类型,需要用函数返回 info: { default: () => ({ name: 'wise', home: 'China', }), }, },
在上一篇博客《Vue3.x 从零开始(三)—— 使用 Composition API 优化组件》中已经介绍过 setup
如果需要在 setup 中使用 provide,需要引入 Vue 提供的 provide 全局方法:
import { provide, ref } from 'vue'; setup() { // 使用 ref 提供响应性 const author = ref('Wise.Wrong'); // provide 可以定义两个参数,分别为 key 和 value provide('author', author.value); }
同样的,setup 中的 inject 也需要全局引入
import { inject } from 'vue'; setup() { // inject 接收两个参数,分别为 key 和默认值,默认值可以为空 const author = inject('author', '这里是默认值'); return { author }; },
四、子组件传参到子组件
在工作中经常也会遇到平级的两个组件组件的通信
App.vue └─ Home.vue ├─ Header.vue └─ Footer.vue
比如这里的 Header 和 Footer 都是 Home 的子组件,Footer 中有某个字段受 Header 的影响
对于这种情况,应该将交互逻辑放到父组件中处理,这种思路叫做状态提升
比如上图的例子,可以在父组件 Home 中定义一个字段,通过 prop 传入 Footer 组件
然后在 Header 组件中通过 $emit 触发自定义事件,抛出需要传递给 Footer 的数据
同时在 Home 组件中监听该事件,并将接到的参数赋值到传入 Footer 的 prop
子组件与子组件的通信,在实际业务场景中会有所不同,但只要牢记状态提升,将交互逻辑放到父组件来处理,绝大部分情况都能迎刃而解
除了上面提到的情况外,还有可能遇到特别复杂的情况,比如:
1. 孙组件对祖父级组件传参;
2. 孙组件对孙组件传参。
通常来说,这些问题都可以通过良好的组件设计来规避
但随着业务规模的不断扩大,有些复杂的业务场景确实需要直面这些问题,这时候就需要进行状态管理
Vue 团队开发了 Vuex 来集中式存储和管理应用中所有组件的状态
在后面的文章中我会提到如何在 Vue 3 中使用 Vuex,但不会单独介绍 Vuex 的用法
关于 Vuex 的基本用法可以参考我以前的博客《Vue 爬坑之路(四)—— 与 Vuex 的第一次接触》
如果想更进一步,建议阅读 Vuex 的官方文档~