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 的官方文档~

(0)

相关推荐