本文是 Vue3 系列第十三篇,将深入探讨 Vue3 中组件间的通信方式。在组件化开发中,组件之间的数据传递和交互是构建应用的基础。理解不同的通信方式及其适用场景,能够帮助我们构建出结构清晰、可维护性高的组件架构。
一、方式一:props 通信
props 的基本概念
在 Vue 中,组件可以理解为可以复用的模块。但有时候,我们需要让组件根据不同的情况显示不同的内容,这就需要从外部向组件传递数据。props 就是用来实现这种数据传递的机制。
想象一下,你有一个按钮组件,这个按钮在不同页面上可能需要显示不同的文字。你可以通过 props 告诉按钮组件:"在这个页面上,你应该显示'登录';在另一个页面上,你应该显示'注册'"。这就是 props 的作用:让父组件告诉子组件应该怎么显示。
准备父组件和子组件
让我们先创建两个组件:一个父组件和一个子组件。父组件就像是家长,子组件就像是孩子。家长可以给孩子一些东西(数据),孩子也可以告诉家长一些事情(通过回调函数)。
首先创建子组件:
html
<!-- Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
</div>
</template>
然后创建父组件:
html
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<Child />
</div>
</template>
<script setup>
import Child from './Child.vue'
</script>
现在我们有了一对最基本的父子组件,父组件中包含了子组件。接下来,我们将通过 props 在这两个组件之间传递数据。
父组件给子组件传递数据
假设我们想让父组件给子组件传递一个数字,让子组件显示这个数字。这就像是家长给孩子一个苹果,孩子接收并展示这个苹果。
首先,在父组件中,我们需要把数据传递给子组件:
html
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<!-- 给子组件传递一个数字 -->
<Child :a="1" />
</div>
</template>
<script setup>
import Child from './Child.vue'
</script>
这里,我们在使用 Child 组件时添加了 :a="1" 这个属性 。这个属性的意思是:给子组件传递一个名为 a 的 prop,值为数字 1 。注意,这里的冒号 : 很重要,它表示这是一个动态绑定,即使我们传递的是固定值,使用冒号也能确保 Vue 正确识别这个值的类型。
现在,子组件需要接收这个数据。让我们修改子组件:
html
<!-- Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<!-- 显示从父组件接收到的数据 -->
<p>从父组件接收到的数字:{{ a }}</p>
</div>
</template>
<script setup>
// 使用 defineProps 接收父组件传递的数据
defineProps(['a'])
</script>
在子组件中,我们使用 defineProps(['a']) 来声明接收一个名为 a 的 prop。这就像是孩子伸出手说:"我可以接收名为 a 的东西"。当父组件传递数据过来时,子组件就能接收到,并在模板中使用 {``{ a }} 来显示这个值。
defineProps****是一个编译器宏 ,它在编译阶段会被处理。我们传入一个数组 ['a'],表示我们期望接收一个名为 a 的 prop。这样,在子组件中,我们就可以像使用普通数据一样使用这个 prop 了。
子组件给父组件传递数据(通过 props 传递函数)
有时候,我们不仅需要父组件给子组件传递数据,还需要子组件给父组件传递数据。这就像是孩子不仅接收家长给的东西,还需要告诉家长一些事情。
在 Vue 中,有一种巧妙的方式可以实现这种反向通信:父组件把自己的一个方法传递给子组件,子组件调用这个方法,从而把数据"传递"给父组件。
首先,在父组件中定义一个方法,然后把这个方法传递给子组件:
html
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<p>从子组件接收到的数据:{{ dataFromChild }}</p>
<!-- 把 getB 方法传递给子组件 -->
<Child :sendB="getB" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// 准备一个变量来存储从子组件接收的数据
const dataFromChild = ref('')
// 定义一个方法,用于接收子组件传递的数据
const getB = (value) => {
dataFromChild.value = value
console.log('从子组件接收到的数据:', value)
}
</script>
这里,我们定义了一个名为 getB 的方法。这个方法的作用是接收一个参数,把这个参数保存到 dataFromChild 变量中,并在控制台打印出来。然后,我们通过 :sendB="getB" 把这个方法传递给子组件。注意,我们给 prop 起名为 sendB,但传递的值是 getB 这个函数。
现在,让我们看看子组件如何使用这个函数:
html
<!-- Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<p>从父组件接收到的数字:{{ a }}</p>
<button @click="sendDataToParent">向父组件传递数据</button>
</div>
</template>
<script setup>
// 接收父组件传递的 props
const props = defineProps(['a', 'sendB'])
// 定义一个方法,用于向父组件传递数据
const sendDataToParent = () => {
// 调用父组件传递过来的函数
props.sendB('这是来自子组件的数据')
}
</script>
在子组件中,我们通过 defineProps(['a', 'sendB']) 同时接收两个 props:a 是父组件传递的数字,sendB 是父组件传递的函数。
当用户点击按钮时,sendDataToParent 方法会被调用。这个方法中,我们通过 props.sendB('这是来自子组件的数据') 调用了父组件传递过来的函数,并传递了一个字符串作为参数。
这个过程是这样的:子组件调用父组件给它的函数,并传递一些数据。父组件中的函数被调用,接收到子组件传递的数据,然后处理这些数据。通过这种方式,子组件就成功地把数据"传递"给了父组件。
二、方式二:自定义事件
自定义事件的概念
除了使用 props 传递函数来实现子组件向父组件通信,Vue 还提供了另一种更直接的方式:自定义事件。这种方式更符合事件驱动的思维模式。
让我们通过一个比喻来理解自定义事件:想象一下,你有一个门铃(子组件),当有人按门铃时,门铃会发出声音(触发事件),房间里的人(父组件)听到声音后就会去开门(执行回调函数)。自定义事件就是这样的机制:子组件"发出声音"(触发事件),父组件"听到声音"后执行相应的操作。
自定义事件的基本使用
首先,我们来看看如何在父组件中监听子组件的自定义事件:
html
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<p>从子组件接收到的数据:{{ dataFromChild }}</p>
<!-- 监听子组件的自定义事件 send-b -->
<Child @send-b="getB" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const dataFromChild = ref('')
// 事件处理函数
const getB = (value) => {
dataFromChild.value = value
console.log('从子组件接收到的数据:', value)
}
</script>
这里,我们使用 @send-b="getB" 来监听子组件的 send-b 事件。这个语法与监听原生 DOM 事件(如 @click)非常相似。@ 符号是 v-on 的简写,send-b 是事件名,getB 是事件触发时要执行的函数。
理解 $event
在自定义事件中,我们经常需要获取子组件传递过来的数据。Vue 为我们提供了一个特殊的变量 $event,它代表了事件对象或者子组件传递的数据。
html
<!-- 使用 $event 获取子组件传递的数据 -->
<Child @send-b="dataFromChild = $event" />
在这个例子中,当子组件触发 send-b 事件并传递数据时,$event 就是子组件传递的数据,我们直接把它赋值给 dataFromChild。这是一种更简洁的写法。
子组件如何触发自定义事件
现在,让我们看看子组件如何定义和触发自定义事件:
html
<!-- Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<button @click="sendDataToParent">向父组件传递数据</button>
</div>
</template>
<script setup>
// 声明自定义事件
const emit = defineEmits(['send-b'])
const sendDataToParent = () => {
// 触发自定义事件,并传递数据
emit('send-b', '这是来自子组件的数据')
}
</script>
在子组件中,我们使用 defineEmits(['send-b']) 来声明这个组件可以触发哪些自定义事件。这就像是告诉 Vue:"我这个组件有一个 send-b 事件,我可能会在某些时候触发它"。
defineEmits 返回一个 emit 函数,我们可以用这个函数来触发事件。在 sendDataToParent 方法中,我们调用 emit('send-b', '这是来自子组件的数据'),这表示触发 send-b 事件,并传递一个字符串作为数据。
整个过程可以这样理解:子组件说"我要触发 send-b 事件了",然后父组件听到这个事件,就执行 getB 函数。子组件还可以顺便说一句话(传递数据),父组件通过 $event 或者函数参数听到这句话。
自定义事件的命名规范
你可能会注意到,我们在模板中使用的是 send-b(带连字符),但在 JavaScript 代码中使用的是 'send-b'(字符串)。这是 Vue 的约定:在模板中,事件名应该使用 kebab-case(短横线分隔),在 JavaScript 代码中,我们可以使用字符串形式。
这种命名方式与 HTML 的属性命名保持一致,因为模板最终会被编译成 HTML。虽然 Vue 也支持在模板中使用 camelCase(驼峰命名),但为了保持一致性,建议在模板中使用 kebab-case。
三、refs 与 parent
在 Vue3 中,我们有时需要直接访问组件实例,而不是通过 props 或事件进行通信。这就像家长需要直接检查孩子的书包,或者孩子需要直接向家长要东西一样。Vue3 提供了 $refs 和 $parent 来实现这种直接的组件访问。
1. $refs:父组件获取所有子组件的数据和方法
$refs 允许父组件访问所有打了 ref 标签的子组件。这就像是家长可以同时检查所有孩子的书包一样。
第一步:给子组件打上 ref 标识
首先,我们在父组件中给每个子组件打上 ref 标识:
html
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<!-- 给第一个子组件打上 ref 标识 -->
<Child1 ref="c1" />
<!-- 给第二个子组件打上 ref 标识 -->
<Child2 ref="c2" />
<button @click="showChildrenData">验证是否拿到子组件数据</button>
<button @click="getAllChildren($refs)">使用 $refs 获取所有子组件</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
// 创建 ref 变量,名称必须与模板中的 ref 属性值一致
const c1 = ref()
const c2 = ref()
// 父组件自己的数据
const parent = ref("这是父组件的数据")
// 方法1:通过 ref 变量访问子组件数据
const showChildrenData = () => {
console.log('第一个子组件的数据:', c1.value?.son)
console.log('第二个子组件的数据:', c2.value?.son)
}
// 方法2:通过 $refs 访问所有子组件
const getAllChildren = (refs: any) => {
console.log('$refs 对象:', refs)
// 遍历 $refs 对象中的所有子组件
for (let key in refs) {
console.log(`${key} 的数据:`, refs[key].son)
}
}
// 暴露给子组件访问的数据
defineExpose({ parent })
</script>
代码详细解释:
在父组件中,我们做了以下几件事情:
-
给子组件打标签 :通过
ref="c1"和ref="c2"给两个子组件打上唯一的标识 -
创建 ref 变量 :在脚本中创建
const c1 = ref()和const c2 = ref(),这些变量会被 Vue 自动绑定到对应的子组件实例 -
通过 ref 变量访问 :通过
c1.value和c2.value可以访问到子组件实例 -
**使用 refs** :`refs` 是一个特殊的对象,包含了所有打了 ref 标签的子组件
第二步:在子组件中暴露数据
子组件需要明确哪些数据可以让父组件访问,这需要使用 defineExpose:
html
<!-- Child1.vue -->
<template>
<div class="child1">
<h3>第一个子组件</h3>
<p>子组件数据:{{ son }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 子组件的数据
let son = ref("这里是第一个子组件的数据")
// 子组件的方法
const childMethod1 = () => {
console.log('第一个子组件的方法被调用')
}
// 暴露给父组件访问的数据和方法
defineExpose({
son,
childMethod1
})
</script>
html
<!-- Child2.vue -->
<template>
<div class="child2">
<h3>第二个子组件</h3>
<p>子组件数据:{{ son }}</p>
<button @click="showParentData">点击获取父组件数据</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 子组件的数据
let son = ref("这里是第二个子组件的数据")
// 子组件的方法
const childMethod2 = () => {
console.log('第二个子组件的方法被调用')
}
// 获取父组件数据的方法
const showParentData = () => {
// 这里我们稍后会讲解如何获取父组件数据
}
// 暴露给父组件访问的数据和方法
defineExpose({
son,
childMethod2,
showParentData
})
</script>
代码详细解释:
在子组件中,我们做了以下几件事情:
-
定义数据和方法:像普通组件一样定义数据和方法
-
使用 defineExpose :通过
defineExpose明确告诉 Vue:这些数据和方法可以被父组件访问 -
选择性暴露:只暴露需要让父组件访问的内容,保持组件的封装性
第三步:理解 $refs 对象
当你在父组件中点击"使用 refs 获取所有子组件"按钮时,控制台会显示 `refs` 对象的内容。让我们详细理解这个对象:
TypeScript
// $refs 对象的结构大致如下:
{
c1: {
son: "这里是第一个子组件的数据",
childMethod1: function() { ... }
},
c2: {
son: "这里是第二个子组件的数据",
childMethod2: function() { ... },
showParentData: function() { ... }
}
}
重要特性:
-
响应式对象 :
$refs是一个 reactive 响应式对象 -
键名对应:对象的键名就是在模板中设置的 ref 属性值("c1"、"c2")
-
值是对应组件:每个键对应的值就是对应子组件实例
-
访问方式:
-
$refs.c1:访问第一个子组件 -
$refs["c1"]:也可以用字符串形式访问 -
$refs.c1.son:访问第一个子组件的 son 数据 -
$refs.c1.childMethod1():调用第一个子组件的方法
-
2. $parent:子组件获取父组件的数据和方法
$parent 允许子组件访问父组件的实例。这就像是孩子可以直接找家长要东西一样。
第一步:父组件暴露数据
首先,父组件需要暴露一些数据和方法给子组件访问:
TypeScript
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<p>父组件数据:{{ parentData }}</p>
<Child1 ref="c1" />
<Child2 ref="c2" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
// 父组件的数据
const parentData = ref("这是父组件的数据")
// 父组件的方法
const parentMethod = () => {
console.log('父组件的方法被调用')
parentData.value = "父组件方法修改后的数据"
}
// 暴露给子组件访问
defineExpose({
parentData,
parentMethod
})
</script>
第二步:子组件通过 $parent 访问父组件
TypeScript
<!-- Child2.vue 模板中使用 -->
<template>
<div class="child2">
<h3>第二个子组件</h3>
<button @click="showParentData($parent)">在模板中获取父组件</button>
</div>
</template>
<script setup lang="ts">
const showParentData = (parent: any) => {
console.log('通过模板传递的 $parent:', parent)
if (parent && parent.exposed) {
console.log('父组件数据:', parent.exposed.parentData)
}
}
</script>
使用 refs 和 parent 的注意事项
虽然 $refs 和 $parent 提供了直接访问组件实例的能力,但它们应该谨慎使用,原因如下:
-
破坏了组件的封装性:组件应该是独立的、封装的模块。直接访问内部实现破坏了这种封装。
-
增加了耦合度 :使用
$refs和$parent会让组件之间更加紧密地耦合在一起,不利于维护和测试。 -
可能导致难以调试的问题:当组件关系复杂时,直接访问可能带来意想不到的副作用。
一般来说,我们应该优先使用 props 和自定义事件进行组件通信。只有当确实需要直接访问组件实例时,才考虑使用 $refs 或 $parent。
四、总结
通过本文的学习,我们了解了 Vue3 中组件通信的几种主要方式:
-
props:父组件向子组件传递数据的基本方式,也可以用于子组件向父组件传递数据(通过传递函数)。
-
自定义事件:子组件向父组件传递数据的更直接方式,符合事件驱动的编程模型。
-
$refs:父组件直接访问子组件实例的方式,应该谨慎使用。
-
$parent:子组件直接访问父组件实例的方式,同样应该谨慎使用。
在实际开发中,我们应该根据具体需求选择合适的通信方式。大多数情况下,props 和自定义事件已经足够满足需求。它们提供了清晰的接口,使得组件之间的关系更加明确,也更易于维护和测试。
关于 Vue3 组件通信有任何疑问?欢迎在评论区提出,我们会详细解答!