在编程的世界里,组件之间的沟通就像家庭成员之间的交流一样重要。 当你开始构建一个Vue.js应用时,你很快就会发现,不同组件之间需要相互通信才能协同工作。今天,我们将一起探索Vue.js中组件间的通信技巧。
接下来,我将通过一个简单的todos demo来详细介绍五种父子间的通信方式。如果你对Vue的父子通信有点陌生,那么让我们一起开始这段旅程吧!
首先是最简单的 props传值,
1. 当爹的总是要给儿子点东西,对吧? ------ 用props给子组件传递数据的艺术。
在Vue中,数据传递就像家庭中的传统:父亲总是会给儿子一些东西。这就是我们所说的props,它是Vue组件的一个自定义属性,用于接收从父组件传递过来的数据。
Props(属性)
props
是Vue组件的一个自定义属性,用于接收从父组件传递过来的数据。在子组件中定义props
,可以使得父组件的数据能够在子组件中使用。
如何使用Props进行父子通信
步骤 1: 在子组件中定义Props
在子组件中定义需要接收的props
。这可以通过在组件的<script setup>
部分使用defineProps
来完成。
vue
// Child.vue
<template>
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
// 定义一个名为 list 的 prop,类型为 Array,默认值为空数组
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
</script>
<style lang="css" scoped>
</style>
在这个例子中,我们定义了一个名为list
的prop
,它的类型是Array
,并且有一个默认值是一个空数组。这意味着即使父组件没有传递任何数据,子组件仍然可以正常工作,因为它已经有了一个默认的列表。
步骤 2: 在父组件中传递数据
在父组件的模板中,将数据绑定到子组件的props
上。这通常通过在子组件的标签上使用冒号(:
)来表示属性绑定。
vue
// Parent.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child :list="list" />
</template>
<script setup>
import { ref } from 'vue'
import Child from './child.vue'
// 创建一个可响应式变量 newMsg,用于存储新的消息
const newMsg = ref('')
// 创建一个可响应式的数组 list,用于存储所有消息
const list = ref(['html', 'css'])
// 添加新消息到 list 数组中
const add = () => {
list.value.push(newMsg.value)
newMsg.value = ''
}
</script>
<style lang="css" scoped>
</style>
在父组件中,我们创建了一个名为newMsg
的响应式变量,用于存储用户输入的新消息。同时,我们还创建了一个名为list
的响应式数组,用于存储所有的消息。当用户点击"确定"按钮时,add
函数会被调用,将newMsg
的值添加到list
数组中,并清空newMsg
。
接着,我们使用<Child :list="list" />
这样的形式将list
数组传递给子组件Child
。冒号(:
)表示我们正在绑定一个属性,这里的属性名是list
,而它的值是父组件中的list
变量。
注意:
-
验证 Props: 除了定义
type
和default
之外,你还可以为props
定义更多的验证规则。例如,你可以定义一个最小长度、最大长度等。javascriptconst props = defineProps({ list: { type: Array, required: true, // 必须传递 validator: (value) => value.length >= 3 // 验证列表长度至少为3 } })
-
Props 的动态绑定: 你可以使用动态属性绑定的方式将
props
绑定到子组件上。这种方式特别适用于需要传递多个props
的情况。vue<Child v-bind="{ ...propsObject }" />
-
Props 的解构: 如果子组件中有多个
props
,你可以使用解构赋值的方式简化代码。javascriptconst { list } = defineProps({ list: { type: Array, default: () => [] } })
-
Props 的注意事项:
- 不要在子组件中直接修改
props
的值。 - 如果需要根据
props
的值做出改变,可以使用计算属性或方法。 - 使用
props
时,确保遵循Vue的命名约定,使用小驼峰式命名法。
- 不要在子组件中直接修改
相信大家在写项目的时候都遇到过类似场景,
"应用中有多个组件,包括一个头部组件(Header),用于添加新的待办事项;一个主体组件(Body),用于显示待办事项列表;还有一个底部组件(Footer),用于显示待办事项的状态统计信息。头部组件需要将新添加的待办事项通知给主体组件,以便实时更新待办事项列表。此外,主体组件还需要将待办事项列表的变化通知给底部组件,以便实时更新状态统计信息。"。
这时就需要子组件更改父组件传来的数据了,不知道大家知不知道其实在vue3中 子组件是可以直接改父组件传来的数据的,而且父组件也是可以感应到的。但是官方不推荐直接在子组件中更改父组件传来的数据,因为这样违背了尤雨溪的设计理念------单向数据流,这时候就需要发布订阅的机制了。
2. 儿子长大了,也要学会跟爹汇报工作了! ------ 通过发布订阅机制让子组件向父组件传递信息。
在Vue.js中,子组件可以向父组件传递信息,就像儿子向父亲汇报工作一样。这就是发布订阅机制,子组件通过$emit触发事件,父组件通过v-on或@监听这些事件。
发布订阅机制的基本概念
在Vue中,组件实例提供了一个自定义事件系统,允许组件实例触发和监听事件。这类似于发布订阅模式,其中:
- 发布 :子组件通过
$emit
方法触发一个事件,并可以传递数据。 - 订阅 :父组件通过
v-on
或@
指令监听子组件触发的事件。
如何使用发布订阅机制进行子父通信
步骤 1: 子组件发布事件
在子组件中,当你需要向父组件传递信息时,可以使用emit
方法来触发一个事件。你可以给事件命名,并且可以传递数据作为参数。
vue
// Child.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { defineEmits } from 'vue'
const newMsg = ref('')
const emits = defineEmits(['addMsg'])
const add = () => {
emits('addMsg', newMsg.value)
newMsg.value = ''
}
</script>
<style lang="css" scoped>
</style>
在这个子组件中,我们首先导入了ref
和defineEmits
。然后定义了一个响应式的变量newMsg
用于存储新的消息,以及一个名为emits
的对象,用于触发事件。
当用户点击"确定"按钮时,add
函数会被调用。在这个函数中,我们使用emits('addMsg', newMsg.value)
来触发一个名为addMsg
的事件,并将newMsg
的值作为参数传递给这个事件。最后,我们将newMsg
的值清空,以便用户可以继续输入新的消息。
步骤 2: 父组件订阅事件
在父组件中,你可以使用v-on
或@
指令来监听子组件触发的事件。当事件被触发时,可以执行一个方法来处理传递过来的数据。
vue
// Parent.vue
<template>
<Child @addMsg="add" />
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './child.vue'
const list = ref(['html', 'css'])
const add = (msg) => {
list.value.push(msg)
}
</script>
<style lang="css" scoped>
</style>
在父组件中,我们首先导入了ref
和子组件Child
。接着,我们创建了一个响应式的数组list
,用于存储所有的消息。
当子组件触发addMsg
事件时,父组件中的add
函数会被调用,并且该函数接收一个参数msg
,这是子组件通过$emit
传递过来的数据。在这个函数中,我们将msg
添加到list
数组中,这样就可以在父组件中显示新的消息了。
注意:
-
事件名称:
- 子组件触发的事件名称必须是唯一的,以避免与内置事件或其它自定义事件冲突。
-
传递多个参数:
- 你可以在
$emit
方法中传递多个参数,只需要在括号内列出即可。
javascriptemits('addMsg', newMsg.value, someOtherData)
- 你可以在
-
动态事件名:
- 你还可以使用动态事件名,这对于需要动态绑定事件名的场景非常有用。
vue<Child :@event-name="add" />
-
事件修饰符:
- Vue提供了事件修饰符,如
.stop
、.prevent
等,可以用来控制事件的行为。
vue<Child @addMsg.stop="add" />
- Vue提供了事件修饰符,如
-
事件监听器的注意事项:
- 如果事件监听器中包含多个方法,这些方法将会按照它们被声明的顺序依次调用。
- 你可以使用
$off
方法来移除事件监听器。
-
事件的命名空间:
- 对于复杂的事件结构,你可以使用命名空间来组织事件,避免命名冲突。
javascriptemits('namespace:addMsg', newMsg.value)
3. 双向绑定,让父子沟通无障碍! ------ 使用v-model实现子父间的双向数据绑定。
当儿子长大了,他们也需要学会如何跟爹进行双向沟通。在Vue.js中,这就是通过v-model实现的。 在Vue.js中,v-model
是一个指令,用于在表单输入和应用状态之间创建双向数据绑定。除了在单个组件内部使用外,v-model
也可以用于父子组件之间的通信,实现子组件向父组件传递数据的功能。
对于需要实现双向数据绑定的情况,v-model提供了一种简洁的方式来处理子父间的通信。
V-Model 在组件中的工作原理
在组件中使用 v-model
时,Vue 会做以下事情:
- 将
v-model
的值传递给子组件的props
,默认情况下是名为modelValue
的 prop。 - 当子组件内部发生数据变化时,通过
$emit
触发一个事件,默认情况下是update:modelValue
事件,并将新的值作为参数传递给父组件。
如何使用 V-Model 实现子父通信
步骤 1: 子组件接收 modelValue
Prop
在子组件中,你需要定义一个 props
来接收 v-model
传递的值。此外,你还需要在子组件中定义一个方法来处理数据的变化,并且需要使用defineExpose
来暴露子组件的list
属性。
vue
// Child.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newMsg = ref('')
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue'])
const add = () => {
props.list.push(newMsg.value)
newMsg.value = ''
emit('update:modelValue', props.modelValue)
}
defineExpose({ list: props.list }) // 子组件心甘情愿暴露出来
</script>
<style lang="css" scoped>
</style>
在这个子组件中,我们首先定义了一个props
对象,其中包含了list
prop,它的类型是Array
,并且有一个默认值是一个空数组。这样,当父组件使用v-model
绑定子组件时,子组件就能接收到这个数组。
接下来,我们定义了一个emit
对象,用于触发事件。当用户点击"确定"按钮时,add
函数会被调用。在这个函数中,我们首先将newMsg
的值添加到list
数组中,然后清空newMsg
的值。最后,我们通过emit('update:list', props.list)
来触发一个update:list
事件,并将更新后的数组作为参数传递出去。
步骤 2: 父组件使用 v-model
绑定
在父组件的模板中,你可以使用 v-model
指令绑定子组件,这样当子组件触发 update:list
事件时,父组件的绑定数据会自动更新。或者使用@update:list='list'
vue
// Parent.vue
<template>
<Child v-model:list="list" />
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './child.vue'
const list = ref(['html', 'css'])
</script>
<style lang="css" scoped>
</style>
在父组件中,我们首先定义了一个响应式的数组list
,用于存储所有的消息。接着,我们使用<Child v-model:list="list" />
这样的形式将list
数组传递给子组件Child
。v-model
指令告诉Vue使用list
作为modelValue
prop的值,并且当子组件触发update:list
事件时,父组件中的list
也会相应地更新。
拓展:
-
自定义模型选项:
- 你可以通过在子组件中定义
model
选项来自定义v-model
使用的prop
和事件名称。
javascriptdefineProps({ customModelValue: { type: Array, default: () => [] }, model: { prop: 'customModelValue', event: 'update:customModelValue' } })
- 你可以通过在子组件中定义
-
动态绑定:
- 你可以使用动态绑定的方式来自定义
v-model
绑定的prop
和事件名称。
vue<Child v-model:customModelValue="list" />
- 你可以使用动态绑定的方式来自定义
-
多值绑定:
- 有时候,你可能需要使用
v-model
来绑定多个值。在这种情况下,你可以使用v-model
的语法糖。
vue<Child v-model:value1="value1" v-model:value2="value2" />
- 有时候,你可能需要使用
-
非标准元素:
v-model
不仅限于表单元素,还可以用于自定义组件和其他元素,如<select>
、<textarea>
等。
4. 想跟儿子说话?直接喊他名字就好! ------ 通过ref实现子父通信的小技巧。
有时候,当爹的想要跟儿子说话,直接喊他的名字就可以了。在Vue.js中,这就是通过ref实现的子父通信。
使用 Ref 进行子父通信的基本概念
ref
是一个特殊的属性,当用在Vue组件上时,它允许你直接访问该组件的实例。通过这种方式,父组件可以调用子组件的方法,或者访问和修改子组件的数据。
如何使用 Ref 实现子父通信
步骤 1: 在父组件中为子组件定义 Ref
在父组件的模板中,你需要在子组件上添加一个ref
属性,并为它指定一个唯一的名字。
vue
// Parent.vue
<template>
<Child ref="childRef" />
<div class="body">
<ul>
<li v-for="item in childRef?.list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import Child from './child.vue'
const childRef = ref(null)
onMounted(() => {
console.log('Child component list:', childRef.value?.list)
})
</script>
<style lang="css" scoped>
</style>
在这个父组件中,我们首先定义了一个ref
变量childRef
,用于存储子组件的引用。接着,我们在子组件Child
上使用了ref="childRef"
,这样Vue就会在组件实例化后将其存储在childRef
中。
onMounted
钩子会在组件挂载完成后被调用,我们可以在这里安全地访问childRef
的值。在这个例子中,我们只是简单地打印了子组件的list
属性,以展示如何访问子组件的属性。
步骤 2: 在子组件中暴露属性或方法
在子组件中,你需要使用defineExpose
来暴露属性或方法,以便父组件可以通过ref
访问它们。
vue
// Child.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
newMsg.value = ''
}
defineExpose({ list })
</script>
<style lang="css" scoped>
</style>
在这个子组件中,我们定义了一个响应式的数组list
,用于存储所有的消息。我们还定义了一个add
函数,用于将新的消息添加到list
数组中。
最后,我们使用defineExpose({ list })
来暴露list
属性,这样父组件就可以通过ref
来访问它。
注意
ref
只在组件渲染完成后才填充,并且它是非响应式的。因此,你应当避免在模板或计算属性中直接引用ref
。- 如果你在子组件上使用
v-for
,ref
将是一个包含所有子组件实例的数组。 - 使用
ref
进行子父通信可能会使组件之间的耦合度增加,因此应当谨慎使用,并确保不会破坏组件的独立性。
拓展
-
调用子组件的方法:
- 你还可以通过
ref
调用子组件的方法。只需确保子组件中使用defineExpose
暴露了该方法。
javascript// Child.vue defineExpose({ add }) // Parent.vue childRef.value.add()
- 你还可以通过
-
动态
ref
:- 你可以在
ref
属性中使用表达式,这样可以根据不同的条件动态地选择子组件。
vue<Child ref="childRefs[currentTab]" />
- 你可以在
-
多层级组件:
- 如果你有多个层级的组件,你可以通过链式调用来访问更深层次的组件。
javascriptgrandParentRef.value.childRef.value.grandChildRef.value
当我们需要跨越多个组件层级传递数据时,使用上面的链式调用就显得不是那么优雅了,那么provide和inject就成为了解决这一问题的有效工具。
5. 家族传承,从爷爷到孙子,一代代往下传! ------ 利用provide和inject实现跨层级组件通信的妙招。
provide和inject就像是家族中的传统,一代代往下传。 provide
和 inject
是Vue.js提供的一种依赖注入方式,它允许一个祖先组件向其所有子孙组件提供数据或方法,而不需要通过逐层传递props
。这种机制特别适合于跨多个组件层级共享数据的情况。
Provide/Inject的基本概念
provide
和 inject
是Vue.js提供的一种依赖注入方式,它允许一个祖先组件向其所有子孙组件提供数据或方法,而不需要通过逐层传递props
。
Provide
provide
选项允许你定义一个可以被后代组件注入的数据或方法。你可以在组件的provide
选项中提供一个对象,或者在组件的setup
函数中使用provide
函数。
Inject
inject
选项用于接收祖先组件通过provide
提供的值。你可以在组件的inject
选项中指定需要接收的数据或方法,或者在组件的setup
函数���使用inject
函数。
如何使用Provide/Inject
步骤 1: 在祖先组件中提供数据或方法
在祖先组件中,你需要使用provide
来定义要提供给子孙组件的数据或方法。
vue
// Parent.vue
<template>
<Child />
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import Child from './child.vue'
const list = ref(['html', 'css'])
provide('list', list)
</script>
<style lang="css" scoped>
</style>
在这个父组件中,我们定义了一个响应式的数组list
,用于存储所有的消息。接着,我们使用provide('list', list)
来提供这个数组,这样子孙组件就可以通过inject
来获取它。
步骤 2: 在子孙组件中注入数据或方法
在需要使用这些数据或方法的子孙组件中,使用inject
来接收这些值。
vue
// Child.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref, inject } from 'vue'
const newMsg = ref('')
const list = inject('list')
const add = () => {
list.value.push(newMsg.value)
newMsg.value = ''
}
</script>
<style lang="css" scoped>
</style>
在这个子组件中,我们使用inject('list')
来接收从父组件提供的list
数组。接着,我们定义了一个add
函数,用于将新的消息添加到list
数组中。
注意事项
provide
和inject
的关系是基于组件的层级结构的,只有当组件在提供者的后代组件中时,它才能注入所提供的数据或方法。- 提供的数据如果是响应式的(例如使用
ref
或reactive
),那么注入它的组件将能够响应这些数据的变化。 provide
和inject
的绑定不是响应式的。如果你传入了一个响应式的ref
值,那么它是作为ref
对象传入的,而不是作为它的内部值。- 使用
provide
和inject
时,应当注意不要过度使用,以免造成组件之间的过度耦合。
拓展
-
跨组件层级通信:
provide
和inject
非常适合跨组件层级通信,特别是当你需要将数据传递给多个层级下的组件时。
-
默认值:
- 你可以在
inject
时提供一个默认值,以防万一祖先组件没有提供相应的值。
javascriptconst list = inject('list', ['default'])
- 你可以在
-
命名空间:
- 如果你有多个祖先组件都提供了相同名称的数据,你可以使用命名空间来区分它们。
javascriptprovide('namespace:list', list) const list = inject('namespace:list')
总结
Vue提供了多种方式来实现组件之间的数据传递和状态管理,包括父子通信、子父通信、v-model双向绑定和provide/inject跨组件通信。以下是我对这五种通信方式的总结
- 父子通信 - Props:父组件通过props向子组件传递数据,子组件通过defineProps接收这些数据。这是一种单向的数据传递方式,通常用于简单的数据传递。
- 子父通信 - 发布订阅机制:子组件通过$emit触发事件,父组件通过v-on或@监听这些事件。这种方式更灵活,适用于需要子组件向父组件传递信息的情况。
- 子父通信 - V-Model:v-model创建了表单输入和应用状态之间的双向数据绑定。在父子组件之间使用v-model可以实现子组件向父组件传递数据的功能。
- 子父通信 - 使用 Ref:父组件通过ref直接访问子组件的实例和它的属性或方法。这种方式可以实现更直接的子父组件通信,但可能会增加组件之间的耦合度。
- Provide/Inject:祖先组件通过provide提供数据或方法,子孙组件通过inject接收这些值。这种方式特别适合于跨多个组件层级共享数据的情况。