# vue3 组件之间传值

发布时间 2023-12-29 15:47:25作者: 我是ed

vue3 组件之间传值

非常好,为啥突然开这样一篇博文,首先是因为 vue3 是未来发展的趋势。其次,vue 官方已经确认,将于2023年最后一天停止对 vue2 项目的维护,这个是官方发出的通知,并且呢,尤雨溪团队也已经将 vue3 作为了 vue 的默认版本了,同时呢,无论是 elementUI 和 ant-d 组件库团队,也已经很早之前就发布了针对于 vue3 的组件库。接下来,我们要真正的开始内卷 vue3 了家人们!

前言

vue2.7 是现在,也是最后一个 vue2 版本的更新,官方已经发布通告,vue2 版本将于 2023年12月31日 停止维护,但是停止维护不代表不能使用哈,我们可以继续使用 vue2 版本开发我们的项目,只不过,官方团队已经不会在对 vue2 版本进行更新,这个更新包括了安全性和兼容性的更新修复问题。如果我们继续使用 vue2 版本开发项目的话,我们可能就需要面对一个问题,就是如何向用户解释:你买了我们的电脑,但我们给你配的是 window xp 系统。

在这里插入图片描述

vue3 组件通信

使用过 vue2 的兄弟们,在开发项目里面最常用的东西是啥子嘞?首先组件通信排第一吧!牛的嘞,这一节,就说一下 vue3 的组件通信方式哈。

好的,首先要注意一点,学习这一部分的话需要用到一些其他的知识点:第一个是 vue3 的基础语法,可以看我之前的博文;再一个就是 TypeScript,当然也可以看我之前的博文。都没有问题了,下面的东西就很简单了。

props 传值

在 vue2 里面可以使用 props 传值,在 vue3 里面依旧可以使用,但是改了个名字,叫 defineProps 获取父组件传递的数据,且在组件内部不需要引入 defineProps 方法就可以直接使用。

下面案例稍微讲一下哈,首先我们创建一个 vue3 的项目,我们编写两个组件:

编写父组件

首先编写一个父组件

<!-- 模板语法 -->
<template>
  <p class="ed-father-title">父组件</p>
  <div class="ed-father-con">

  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>
.ed-father-title {
  margin: 10px;
  font-size: 25px;
  color: hotpink;
  font-weight: 550;
}

.ed-father-con {
  height: 400px;
  background-color: beige;
  padding: 15px;
}
</style>

执行效果就是下面的样子:

在这里插入图片描述

编写子组件

接下来继续开发一个组件作为子组件:

<template>
  <p class="ed-son-title">子组件</p>
  <div class="ed-son-con">

  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>
.ed-son-title {
  margin: 10px;
  font-size: 20px;
  color: red;
  font-weight: 550;
}

.ed-son-con {
  height: 300px;
  background-color: cadetblue;
  padding: 15px;
}
</style>

编写完成之后呢,我们在父组件里面引入这个子组件。

父组件引入子组件

引入子组件呢,就需要两个步骤就可以了,首先引入子组件文件,然后在 dom 上使用子组件就可以了。

<!-- 模板语法 -->
<template>
  <p class="ed-father-title">父组件</p>
  <div class="ed-father-con">
    <!-- 使用子组件 -->
    <sonModel></sonModel>
  </div>
</template>

<script setup lang="ts">
// 引入子组件
import sonModel from './sonModel.vue';

</script>

<style scoped>
.ed-father-title {
  margin: 10px;
  font-size: 25px;
  color: hotpink;
  font-weight: 550;
}

.ed-father-con {
  height: 400px;
  background-color: beige;
  padding: 15px;
}
</style>

然后我们看一下效果:

在这里插入图片描述

非常棒,效果已经引入进来了。

父组件向子组件传递参数

首先我们在父组件创建一个变量,然后把这个变量传递给子组件,让子组件接收这个变量。

父组件创建变量

父组件创建一个 num,设置他的值为 10:

// 创建变量
const num = ref(10)

父组件把值传递给子组件

然后呢,我们把这个变量传递给子组件,和 vue2 的方式是一样的:

<!-- 使用子组件 -->
<sonModel :num="num"></sonModel>

这样我们就把父组件的变量传递给子组件了,这里我们传递的是变量,当然我们也可以直接写死传递一个常量进去:

<!-- 使用子组件 -->
<sonModel :num="num" msg="我是ed."></sonModel>

好,到这里应该都明白就不再赘述了。

接下来就是子组件通过 defineProps 方法获取父组件传递进来的数据,同样也是很简单,注意一点,defineProps 方法是不需要引入的,直接使用即可。

子组件接收数据:方式一

let props = defineProps({
  num: {
    type: Number,  // 接收数据的类型
    default: 0   // 默认值
  },
  msg: {
    type: String,  // 接收数据的类型
    default: 'hello'   // 默认值
  }
})

OK,结束了。我们看一下:

在这里插入图片描述

没问题,子组件已经有这两个数据了,我们可以直接在页面上显示一下子:

<template>
  <p class="ed-son-title">子组件</p>
  <div class="ed-son-con">
    <p class="ed-txt">父组件传递的num:{{ num }}</p>
    <p class="ed-txt">父组件传递的msg:{{ msg }}</p>
  </div>
</template>

看一下效果:

在这里插入图片描述

没问题,可以正常使用!这是子组件接受参数的第一种方式。

子组件接收数据:方式二

第二种方式和第一种一样,只不过不需要那么繁琐的配置,简单配置也是可以的了。

// 子组件接收父组件传参方式二
let props = defineProps(['num', 'msg'])

这样是也是可以的,效果是一模一样的,只不过取消了对数据类型的限制,和 vue2 其实是一样的哈!

在这里插入图片描述

页面效果也是一样的。

自定义事件

首先呢,自定义事件也是可以进行组件间传值的,先说一下哈,在 vue 里面嘞,有两种事件,一种是原生DOM事件,一种是自定义事件

原生事件

原生事件都知道的,像是 click、dbclick、change、mouseenter 这些都是原生事件。

比如下面的代码:

<div @click="divClickFun"></div>

给 div 标签绑定了一个原生事件 click,他默认会给事件回调注入 enent 事件对象,当然点击事件想注入多个参数也是可以的,但是切记,注入的事件对象必须叫做 $event

<div  @click="divClickFun(10,'我是ed.', $event)">点击</div>

我们看一下打印的结果:

在这里插入图片描述

如果我们传递的不是以 $event 命名的话,我们看一下:

<div  @click="divClickFun(10,'我是ed.', event)">点击</div>

看一下打印的数据:

在这里插入图片描述

就是 undefined 了,所以注意:注入的事件对象务必叫做 $event。

在 vue3 框架里面呢,click、dbclick、change 这类原生DOM事件,无论是在标签还是组件上面都算是原生DOM事件;但是在 vue2 中不是这样的,在 vue2 中组件标签上面,需要通过 native 修饰符才能变成原生DOM事件,这个知道就可以哈!

自定义事件

自定义事件,可以实现子组件给父组件传递数据,在项目中是非常非常重要的,也是经常使用到的。

比如最开始的案例,我们在父组件引入的子组件标签上追加一个自定义事件:

<sonModel @getData="getDataFun" :num="num" msg="我是ed."></sonModel>

上面这个代码写 vue2 的应该都知道哈,就不赘述了,就是自己写一个自定义事件 getData ,触发之后调用 getDataFun 方法。

那么怎么触发这个自定义事件 getData 呢?

我们需要在子组件内部触发,比如说我们在子组件内部编写一个按钮,点击按钮的时候,触发这个自定义事件,并且传值。

子组件内部编写按钮

<button @click="toFatherData()">向父组件传递数据</button>

编写一个按钮,有一个点击事件,点击执行 toFatherData() 方法。

使用 defineEmits 方法

接下来我们需要了解一个新的方法,叫做 defineEmits ,这是 vue3 提供的一个方法,不需要引入,直接使用即可。这个方法需要传递一个数组进去,数组的元素就是将来数组需要触发的自定义事件类型,这个方法会返回一个 $emit 方法来触发自定义事件。

说的比较多,看下面的代码就可以了:

let $emit = defineEmits(["getData"]);

因为子组件想出发父组件的 getData 传递参数,所以传递列表的数组元素就得包含这个将来可能会触发的自定义事件方法。

触发自定义方法

上面我们已经编写了一个按钮,按钮带有点击事件,$emit 也已经有了,接下来就是触发父组件的自定义事件了,接下来我们完善按钮的点击事件:

// 按钮点击事件
let toFatherData = () => {
  $emit("getData", "我是子组件传递的数据", "大家好,我是ed.");
}

当我们点击按钮的时候,事件回调内部调用 $emit 方法去触发自定义事件,第一个参数为触发事件类型,第二个、三个、N个参数即为传递给父组件的数据。

父组件打印子组件传递的数据

上面一个步骤呢,子组件成功触发了父组件的自定义事件,然后父组件自定义事件触发之后,他会走一个函数,我们在这个函数里面就可以获取到子组件传递回来的数据,我们打印一下:

// 父组件接收打印子组件传递回来的数据
const getDataFun = (data: string, data2: string) => {
  console.log(data, data2);
}

我们看一下打印的结果:

在这里插入图片描述

可以看到,当子组件的按钮被点击,父组件可以正常打印出子组件传递回来的数据了。

注意事项

但是注意一点哈,跟上面案例没关系了,看下面的代码:

<sonModel @click="clickFun" @getData="getDataFun" :num="num" msg="我是ed."></sonModel>

getData 方法是一个自定义事件对吧,click 方法是一个原生事件,但是,如果我们在子组件的 defineEmits 中定义了 click ,那么,click 就被定义成为自定义事件了:

let $emit = defineEmits(["getData", "click"]);

好的,到这里 自定义事件 传递数据就结束了。

全局事件总线传值

上面我们说了使用 propsemit 实现父子组件之间的传值,那就有一个问题,如果是兄弟组件之间通信呢?当然了,使用 props 链也是可以的:子组件1把数据传递到父组件,父组件在把数据传递给子组件2。这是完全没有问题的,除了复杂一些。

其实还有一种方式能够更快的实现兄弟组件之间传值,那就是全局事件总线,全局事件总线可以实现任意组件间的通信。

在 vue2 里面,可以根据 VM 和 VC 关系推理出全局事件总线,但是 vue3 里面没有 vue 构造函数,也就没有 Vue.prototype. 以及组合式API没有 this ,那在 vue3 里面想要实现全局事件总线功能就有些许的不现实,其实,在 vue3 中如果想使用全局事件总线可以采用 mitt 插件。

mitt

mitt npm 地址:https://www.npmjs.com/package/mitt

这里呢,就单纯的说一下怎么简单的使用哈,详细的大家去上面的网站看哈。

Mitt 有啥优点呢,稍微说一下,就一下:

  • 零依赖,体积小,压缩后就200b。
  • 提供了完整的 TypeScript 的支持,能自动推断参数类型。
  • 基于闭包实现,没有 this 困扰。
  • 为浏览器编写,但也支持其他 JavaScript 。
  • 与框架无关,可以与任何框架搭配使用。

mitt 安装

安装其实也比较简单,直接一行命令就可以安装完成:

npm i mitt

等待命令执行完成就可以了。

使用 mitt

使用的话和 vue2 的 bus 差不多,我们先创建一个 EventBus.ts 文件。

在这里插入图片描述

在文件中创建事件总线对象并对外暴露,然后就可以在使用事件总线的地方导入了。

import mitt from "mitt";
export default mitt();

很简单,就两行代码:

在这里插入图片描述

接下来就是在需要的组件中使用了。

使用的方式比较简单,还是上面案例,我们子组件直接通过 mitt 给父组件通信,不用 emit 了。

子组件发送数据

首先在子组件内部,引入我们上面创建的文件:

import EventBus from '../utils/EventBus';

然后我们重写按钮点击事件,通过 mitt 给父组件传值:

// 按钮点击事件
let toFatherData = () => {
  // $emit("getData", "我是子组件传递的数据", "大家好,我是ed.");
  EventBus.emit('sendData', {name: '我是ed.', age: 18})
}

看上面代码,EventBus.emit( type , evt ) 方法呢,就是用 mitt 发送数据,这个方法有两个参数,一个是 type:要调用的事件类型;一个是 evt:可以理解为传递的参数,建议是对象。

好了,这样的话,子组件就向父组件传递了数据,接下来就是父组件接受子组件传递的数据。

父组件接收数据

同样父组件需要先引入 EventBus 这个文件,任何组件用到就需要引入。

import EventBus from '../utils/EventBus';

引入完成,父组件接收 mitt 传递的数据,需要使用一个方法,叫做 EventBus.on( type, handler ),这个方法也需要两个参数:type 是要调用的事件类型,比如上面子组件发送数据的时候使用的是 sendData,那么父组件如果想接收子组件发送的这个数据,此处也要设置为 sendData;第二个 handler 是回调函数,意思是获取到数据执行什么操作,该函数会有一个回调参数,参数就是传递的数据:

EventBus.on('sendData', (params) => {
  console.log('mitt接收到的数据:', params);
})

OK,上面代码就可以获取组件通过 mitt 传递的数据了,但是有一点需要注意一下,上面这段代码直接写在 setup 里面就行,不需要通过任何操作触发,当收到 sendData 类型的数据就会直接走回调。

在这里插入图片描述

这是子组件给父组件通过 mitt 发送数据,我们只是举了一个例子来实现通过 mitt 通信,其实使用 mitt 按照这个步骤可以实现任何两个组件通信,比如父子组件通信,兄弟组件通信,祖孙组件通信,都可以。

其他注意事项

上面说的只是 mitt 最基本的功能哈,更多消息的功能参考我上面给出的网站。

比如说,我们再在子组件写一个按钮,也是给父组件发送数据,但是我们的类型不是 sendData 了,我们换成一个别的,比如 sendData2,我不细说了直接看代码:

编写 html 代码:

    <button @click="toFatherData2()">向父组件传递数据2</button>

编写 ts 代码:

// 按钮点击事件
let toFatherData2 = () => {
  EventBus.emit('sendData2', { name: '我是ed.同学', age: 24 })
}

然后我们也是在父组件在追加一个监听 sendData2 类型的 mitt 接受数据:

EventBus.on('sendData2', (params) => {
  console.log('mitt接收到的数据2:', params);
})

我们看一下效果:

在这里插入图片描述

点击第一个按钮,打印最开始的数据,点击第二个按钮,打印我们新写的,没有问题。

所以说哈,只要我们这个 type 调用的事件类型匹配起来,我们可以在一个组件里面写好多个,都不影响的。

发现没,我们父组件其实写了两个监听:

在这里插入图片描述

其实 mitt 有一个可以同时监听全部调用事件类型:

EventBus.on('*', (type, params) => {
  console.log('* mitt接收到的数据:', type, params);
})

其实和普通监听是一样的,只不过 调用事件类型的type 变成了 * 号,代表监听全部。同时,回调函数现在回调了两个参数 (type, params) ,其中 type 代表接受到数据的调用事件类型,第二个才是接受的参数,我们可以看一下效果:

在这里插入图片描述

都能打印出来,很好,奈斯,我们写的单独监听的和全部监听的都不会有影响。

好的,还有其他的方法其实,那些去看文档吧,一般也用不到。

好了, mitt 先说这些吧,什么全局挂载全局使用这些,就先不说了, 需要的时候在自行百度吧。

v-model

使用 v-model 可以实现数据双向绑定,除此之外呢,它也可以实现父子组件之间的数据同步。

v-model 实指利用 props[modelValue] 与自定义事件 [update:modelValue] 实现的。

一个案例说明白哈,其实很简单,vue2 里面也有类似的实现方式,vue3 只不过是把写法变了一下,看下面代码:

我先在父组件创建一个变量 pageNo

const pageNo = ref(1)

没问题哈,然后把这个变量双向绑定到子组件:

<sonModel v-model:pageNo="pageNo"  @getData="getDataFun" :num="num" msg="我是ed."></sonModel>

我使用 v-model 绑定到了子组件, 主要是 v-model:pageNo="pageNo" 这段代码,大体意思可以理解为我把父组件的 pageNo 传递到子组件 props 中的 pageNo 里面去。

那么子组件就可以接收父组件传进来的 pageNo 数据:

let props = defineProps(['num', 'msg', 'pageNo'])

然后我们可以看一下子组件 props 里面有没有这个 pageNo

在这里插入图片描述

诶,子组件接受到了,我们可以直接在页面渲染一下子。

<p class="ed-txt">父组件传递的pageNo:{{ pageNo }}</p>

我们看一下,可以正常使用:

在这里插入图片描述

然后我们修改一下父组件这个数据,使用 update:pageNo,子组件中不能直接处理父组件传进来的数据,我们这个其实是通过emit返回给父组件让他来处理:

<button @click="updataPageNoFun()">更新pageNo: {{ pageNo }}</button>

我们在子组件编写一个按钮,按钮有点击事件,点击事件,我们和 emit 一样,触发父组件的自定义事件:

首先,我们要想调用父组件的自定义事件,那么我们必须在 defineEmits 中先把类型添加进来吧?

let $emit = defineEmits(["getData", "update:pageNo"]);

我们添加了一个 update:pageNo 进来,这个是啥意思呢?可以简单理解,我是用update:前缀后,表示我们需要更新pageNo这个数据,因为是 v-model 绑定的数据,父组件直接获取要更新的值给我们更新了,不用在自己编写自定义事件了。

其实 v-model 的本质是属性绑定和事件绑定的结合那语法糖包一下。

然后我们完善按钮点击事件:

const updataPageNoFun = () => {
  $emit("update:pageNo", props.pageNo + 1);
}

我们看一下效果:

在这里插入图片描述
好的,完成了!就这么简单,当然你一个组件绑定多个 v-model 都是可以的,只需要传递的参数不一样就可以了。

useAttrs

vue3 当中,可以使用 useAttrs 来进行父组件向子组件传值。他可以获取组件的属性和方法,当然这包含了原生的DOM事件或者是自定义事件,这个函数类似于 vue2 里面的 $attrs 属性与$listeners 方法。

下面一个案例稍微说一下哈,还是之前的案例,父组件引入子组件,上面向子组件传递了好多数据,包括自定义方法:

<sonModel v-model:pageNo="pageNo"  @getData="getDataFun" :num="num" msg="我是ed."></sonModel>

然后在子组件呢,我们可以使用 useAttrs 来获取子组件上被添加的这些数据和方法,我们先编写一个按钮,点击按钮,打印通过 useAttr 拿到的数据:

<button @click="getDataByUseAttrs()">通过useAttrs获取数据</button>

接下来,我们要引入一下 useAttrs

import { useAttrs } from 'vue';

然后初始化一下:

const $attrs = useAttrs()

然后我们通过点击按钮,打印一下 $attrs 里面的数据:

const getDataByUseAttrs = () => {
  console.log($attrs)
}

我们看一下效果:

在这里插入图片描述

我去,什么也没有打印出来啊!

<sonModel v-model:pageNo="pageNo"  @getData="getDataFun" :num="num" msg="我是ed."></sonModel>

我们不是往子组件绑定了pageNonummsg 这些数据了吗?怎么都没有?

OK, 需要注意如果defineProps接受了某一个属性,useAttrs方法返回的对象身上就没有相应属性与属性值。 因为这些传进来的数据都被 defineProps 接收了,所以这里 $attrs 已经没有了。defineProps 的优先级高于 $attrs

那我们在子组件在传几个数据:

<sonModel ed="我是ed.同学" v-model:pageNo="pageNo"  @getData="getDataFun" :num="num" msg="我是ed."></sonModel>

然后我们在打印一下:

在这里插入图片描述

诶,现在就打印出来了。我们可以使用 console.log($attrs.ed) 直接打印出数据值。

同样,如果挂载了方法的话,也是可以获取到的:

<sonModel @change="sonChangeFun()" ed="我是ed.同学" v-model:pageNo="pageNo"  @getData="getDataFun" :num="num" msg="我是ed."></sonModel>

看一下结果:

在这里插入图片描述

ref 与 $parent

ref

提到 ref 可能会想到它可以获取元素的DOM或者获取子组件实例的VC。既然可以在父组件内部通过ref获取子组件实例VC,那么子组件内部的方法与响应式数据父组件可以使用的。

这个就比较简单了,一个案例结束。

我先把之前案例代码全删掉,从新写哈。

还是父组件调用子组件,然后呢,我在子组件创建几个变量,几个方法。

<script setup lang="ts">

import { ref } from 'vue';

const name = ref('我是ed.');
const age = ref(18);

const changeName = () => {
  console.log("我是子组件的方法")
}

</script>

很简单,我想让父组件能够直接访问子组件的数据和方法,光通过上面的代码不行!

如果让父组件获取子组件的数据或者方法需要通过 defineExpose 对外暴露,因为 vue3 中组件内部的数据对外【关闭的】,外部不能访问。

所以使用 defineExpose 暴露一下:

<script setup lang="ts">

import { ref } from 'vue';

const name = ref('我是ed.');
const age = ref(18);

const changeName = () => {
  console.log("我是子组件的方法")
}

defineExpose({ name, changeName })

</script>

我就暴露了两个哈,age 没有暴露,然后我们在父组件写一个按钮,调用一下看能不能获取到 namechangeName 方法。

首先在父组件,我们先要获取到子组件:

<sonModel ref="sonFrom"></sonModel>
const sonFrom = ref()

然后在父组件写一个按钮,点击按钮的时候,我们打印一下子组件的 name 数据:

<button @click="conData()">打印子组件数据</button>

然后编写点击事件,打印一下子组件的 name

const conData = () => {
  console.log(sonFrom.value.name);
}

我们看一下效果:

在这里插入图片描述

获取成功 !

调用一下子组件的方法:

const conData = () => {
  console.log(sonFrom.value.name);
  sonFrom.value.changeName()
}

看一下结果:

在这里插入图片描述

方法也调用打印出来了。

age 没有在子组件用 defineExpose 抛出来,可以获取到吗?试一下哈:

const conData = () => {
  console.log(sonFrom.value.name);
  sonFrom.value.changeName()
  console.log(sonFrom.value.age);
}

看一下结果:

在这里插入图片描述

好了,关于 ref 就到这里了。

$parent

$parent 可以获取某一个组件的父组件实例VC,因此可以使用父组件内部的数据与方法。必须子组件内部拥有一个按钮点击时候获取父组件实例,当然父组件的数据与方法需要通过defineExpose方法对外暴露。

哎哟,他和 ref 差不多,我不想写了,到这里这篇博文已经写了一万三的字了,好累啊!

不解释了,直接贴代码了哈,和 ref 一样的。

父组件定义一个变量:

const msg = ref('hello world')

defineExpose({ msg })

主要是子组件,写一个按钮,点击按钮的时候打印父组件的数据:

<button @click="getMsg($parent)">获取父组件的 msg 数据</button>

然后编写点击事件:

const getMsg = ($parent) => {
  console.log($parent.msg)
}

查看一下效果:

在这里插入图片描述

成功!好了,到这结束了这一小节,抓紧下一个。

provide 与 inject

vue3提供两个方法provideinject,可以实现隔辈组件传递参数。

这两个其实在 vue2 就是可以使用的,简单点一笔带过哈!

provide方法用于提供数据,此方法执需要传递两个参数,分别提供数据的key与提供数据value

比如父组件向下级组件传递数据:

需要先引入provide

import { provide, ref } from 'vue';

然后传递数据:

provide('play', "我喜欢打篮球")

完成!

然后是在下级组件接收上级组件传递的数据:

同样也是先引入 inject

import { inject, ref } from 'vue';

然后接收数据:

let play = inject("play")

我们看一下有没有:

在这里插入图片描述

哇。没问题,OK,结束了这小节,就这样用。

pinia

这个是集中式管理状态容器,类似于vuex。但是核心概念没有mutationmodules,使用方式参照官网,这里就不说了,之前博客提过这个小菠萝。

这是我之前写的博文,介绍小菠萝的: https://wjw1014.blog.csdn.net/article/details/126008933
这是小菠萝官网:https://pinia.vuejs.org/zh/

插槽

这玩意儿不想写了,尽管可以传值,但是和我想象的传值不是一个意思,有时间单独开一个插槽的博文吧。

好了今天先到这里吧!我要去玩元梦之星了。