所有组件通信的方法,一次学个够
前言
在现代前端开发中,组件化已经成为构建复杂用户界面的主要方法。组件化设计使得应用程序可以被拆分成小的、可复用的模块,每个模块(组件)���以独立开发、测试和维护。然而,在实际开发中,不同组件之间的通信成为了一个重要且不可避免的问题。组件通信的有效管理对于应用的正确性、可维护性和扩展性至关重要。
组件通信的常见方法
-
Props 和 Events:
- 使用
props
传递数据,使用事件(如$emit
)进行反馈,是最常见的父子组件通信方式。
- 使用
-
Provide 和 Inject:
- 用于跨层级通信,父组件使用
provide
提供数据或方法,子组件通过inject
注入。
- 用于跨层级通信,父组件使用
-
Ref 和 DefineExpose:
- 通过
ref
获取子组件实例,并结合defineExpose
暴露方法或数据。
- 通过
-
全局状态管理工具:
- 使用 Vuex 或 Pinia 等状态管理工具,实现复杂应用中的状态共享和管理。
1. 父子通信 ----- 父组件传值,子组件通过defineProps接受
假设我们有一个家庭,其中父母需要给孩子传递一些任务(比如做家务的任务)。在这个例子中:
- 父母是父组件。
- 孩子是子组件。
- 父母给孩子的任务是传递的
props
。
父组件 (ParentComponent.vue)
父母需要给孩子传递一个任务清单。
<template>
<div>
<h1>Parent Component (父母)</h1>
<p>今天的任务:</p>
<!-- 传递 tasks 作为 prop 给子组件 -->
<ChildComponent :tasks="tasksForChild" />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
import {ref} from 'vue'
const tasksForChild= ref(['打扫房间', '洗碗', '做作业'])
</script>
子组件 (ChildComponent.vue)
孩子接收到父母传递的任务,并展示这些任务。
<template>
<div>
<h2>Child Component </h2>
<p>接收到的任务:</p>
<ul>
<li v-for="(task, index) in tasks" :key="index">{{ task }}</li>
</ul>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
// 使用 defineProps 接收来自父组件的 prop
const props = defineProps({
tasks: {
type:Array,
default: () => []
}
});
</script>
解释
在这个例子中,父母(父组件)有一个任务清单 tasksForChild
,包含了几个任务(['打扫房间', '洗碗', '做作业']
)。父母通过 tasks
prop 将这些任务传递给孩子(子组件)。
孩子(子组件)使用 defineProps
接收来自父母的任务,并在页面上展示这些任务。
2. 子父通信 ----- 通过发布订阅机制,父组件订阅一个事件,子组件发布该事件并且将要传递的值一起发布出来,父组件在定义函数中获取该值
我们继续用家庭生活中的例子来说明子组件向父组件通信的过程,通过发布订阅机制实现。
假设我们有一个家庭场景,孩子完成任务后要通知父母。这里,父母是父组件,孩子是子组件。孩子完成任务后,会发布一个事件,父母会订阅这个事件,并获取相关信息。
父组件 (ParentComponent.vue)
父母订阅一个事件,当孩子完成任务时获取通知。
<template>
<div>
<h1>Parent Component (父母)</h1>
<p>{{ messageFromChild }}</p>
<!-- 引入子组件 -->
<ChildComponent @task-completed="handleTaskCompleted" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 父组件定义一个响应式数据来接收子组件传来的消息
const messageFromChild = ref('');
// 定义一个处理子组件事件的方法
const handleTaskCompleted = (task) => {
messageFromChild.value = `孩子完成了任务:${task}`;
};
</script>
子组件 (ChildComponent.vue)
孩子完成任务后,发布一个事件通知父母。
<template>
<div>
<h2>Child Component (孩子)</h2>
<button @click="completeTask('打扫房间')">完成任务</button>
</div>
</template>
<script setup>
import { defineEmits } from 'vue';
// 使用 defineEmits 定义子组件将会触发的事件
const emit = defineEmits(['task-completed']);
// 定义一个方法,当任务完成时触发事件
const completeTask = (task) => {
emit('task-completed', task);
};
</script>
解释
在这个例子中:
-
父组件
ParentComponent.vue
:- 使用
ref
定义响应式变量messageFromChild
,用于存储从子组件接收到的消息。 - 定义
handleTaskCompleted
方法,当子组件触发task-completed
事件时调用此方法,并更新messageFromChild
的值。
- 使用
-
子组件
ChildComponent.vue
:- 使用
defineEmits
定义子组件可能会触发的事件task-completed
。 - 在
completeTask
方法中,使用emit
方法触发task-completed
事件,并传递任务信息(例如"打扫房间"
)。
- 使用
这样,父母(父组件)订阅了孩子(子组件)的完成任务事件,孩子完成任务后通过事件将任务信息传递给父母,父母接收到消息后进行相应的处理。
3. 子父通信 ----- 父组件v-model 绑定属性传给子组件,子组件发布update:xxx 事件通知父组件更新了
这个例子仍然基于家庭生活中的场景,假设父母给孩子一个任务清单,孩子完成任务后通过 v-model
将任务状态通知父母。
v-model为双向绑定
父组件 (ParentComponent.vue)
父母给孩子一个任务,并且通过 v-model
绑定任务的完成状态。
<template>
<div>
<h1>Parent Component (父母)</h1>
<p>任务状态: {{ taskStatus }}</p>
<!-- 使用 v-model 绑定任务状态 -->
<ChildComponent v-model:status="taskStatus" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 定义一个响应式变量存储任务状态
const taskStatus = ref('未完成');
</script>
子组件 (ChildComponent.vue)
孩子完成任务后,通过 update:status
事件通知父母更新任务状态。
<template>
<div>
<h2>Child Component (孩子)</h2>
<button @click="completeTask">完成任务</button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
// 使用 defineProps 接收来自父组件的 v-model 绑定的属性
const props = defineProps({
modelValue: String
});
// 使用 defineEmits 定义子组件将会触发的事件
const emit = defineEmits(['update:modelValue']);
// 定义一个方法,当任务完成时触发事件
const completeTask = () => {
emit('update:modelValue', '已完成');
};
</script>
解释
在这个例子中:
-
父组件
ParentComponent.vue
:- 使用
ref
定义响应式变量taskStatus
,初始值为'未完成'
。 - 使用
v-model:status
绑定taskStatus
变量到子组件的status
属性。 - 当子组件通过
update:status
事件更新任务状态时,父组件会自动更新taskStatus
的值。
- 使用
-
子组件
ChildComponent.vue
:- 使用
defineProps
接收来自父组件的modelValue
属性,这是v-model
绑定的属性。 - 使用
defineEmits
定义update:modelValue
事件。 - 在
completeTask
方法中,使用emit
触发update:modelValue
事件,并将任务状态更新为'已完成'
。
- 使用
这种方式使父母能够实时了解任务的完成情况,不需要等待孩子主动汇报,是一种非常方便的通信方式。
4. 子父通信 ------- 父组件通过ref获取到组件的dom结构,从而获取到子组件defineExpose()
通过 ref
获取子组件的实例,并结合 defineExpose
方法来暴露子组件中的方法或数据,从而实现子组件向父组件通信。通过 ref
获取子组件的 DOM 结构后,可以调用子组件暴露的方法或数据。
以下是一个简单的示例,来说明这种子父通信的方式:
父组件 (ParentComponent.vue)
父母通过 ref
获取到孩子组件的实例,并调用暴露的方法。
<template>
<div>
<h1>Parent Component (父母)</h1>
<button @click="getChildTaskStatus">获取孩子任务状态</button>
<p>任务状态: {{ taskStatus }}</p>
<!-- 通过 ref 绑定子组件 -->
<ChildComponent ref="childComponentRef" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 定义一个 ref 来存储子组件的实例
const childComponentRef = ref(null);
// 定义一个响应式变量来存储任务状态
const taskStatus = ref('');
// 定义一个方法来获取子组件的任务状态
const getChildTaskStatus = () => {
// 调用子组件暴露的方法
taskStatus.value = childComponentRef.value.getTaskStatus();
};
</script>
子组件 (ChildComponent.vue)
孩子通过 defineExpose
暴露一个方法给父母。
<template>
<div>
<h2>Child Component (孩子)</h2>
<button @click="completeTask">完成任务</button>
</div>
</template>
<script setup>
import { ref, defineExpose } from 'vue';
// 定义一个响应式变量来存储任务状态
const taskStatus = ref('未完成');
// 定义一个方法来完成任务
const completeTask = () => {
taskStatus.value = '已完成';
};
// 暴露一个方法给父组件
const getTaskStatus = () => {
return taskStatus.value;
};
// 使用 defineExpose 暴露方法
defineExpose({
getTaskStatus
});
</script>
解释
在这个例子中:
-
父组件
ParentComponent.vue
:- 使用
ref
绑定子组件实例。 - 通过
childComponentRef
获取子组件实例,并调用其暴露的方法getTaskStatus
。 - 将获取到的任务状态存储在响应式变量
taskStatus
中。
- 使用
-
子组件
ChildComponent.vue
:- 使用
ref
定义响应式变量taskStatus
,初始值为'未完成'
。 - 定义方法
completeTask
来更新任务状态。 - 使用
defineExpose
暴露方法getTaskStatus
,让父组件可以访问这个方法。
- 使用
5.父子通信 ----- 父组件provide 提供 子组件去inject注入
假设我们有一个家庭聚餐的场景:
- 父母是父组件,负责准备食物。
- 孩子是子组件,负责获取食物并展示。
父组件 (ParentComponent.vue)
父母提供食物给孩子。
<template>
<div>
<h1>Parent Component (父母)</h1>
<ChildComponent />
</div>
</template>
<script setup>
import { provide } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 父组件准备了一些食物
const food = '美味的披萨';
// 使用 provide 提供食物
provide('food', food);
</script>
子组件 (ChildComponent.vue)
孩子注入并展示食物。
<template>
<div>
<h2>Child Component (孩子)</h2>
<p>孩子拿到了:{{ food }}</p>
</div>
</template>
<script setup>
import { inject } from 'vue';
// 使用 inject 获取父组件提供的食物
const food = inject('food');
</script>
解释
在这个例子中:
-
父组件
ParentComponent.vue
:- 使用
provide
提供一个名为food
的值,代表准备好的食物。 provide('food', food)
将食物传递给子组件。
- 使用
-
子组件
ChildComponent.vue
:- 使用
inject
注入父组件提供的food
。 const food = inject('food')
获取到父组件提供的食物,并在模板中展示。
- 使用
这种方式可以让父组件向多个子组件提供共享的数据或功能,而不需要逐层传递 props
。这种设计在需要跨越多个层级组件时特别有用。但是只能限于辈分大组件给辈分小注射,不能辈分小的给辈分大的注射。
6.兄弟组件之间传值导入pinia定义store直接状态管理
当兄弟组件之间关系不明确,层级复杂时,传值就需要pinia定义好store直接进行状态管理。假设我们有一个购物车场景:
- 我们有两个兄弟组件,一个是商品列表组件,一个是购物车组件。
- 用户可以在商品列表中添加商品到购物车,购物车组件会实时更新显示当前的商品。
安装 Pinia
首先,确保你已经安装了 Pinia:
npm install pinia
定义 Pinia Store
我们需要创建一个 Pinia Store 来管理购物车的状态。
// stores/cart.js
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
addItem(item) {
this.items.push(item);
}
}
});
父组件 (App.vue)
父组件包含商品列表和购物车两个子组件。
<template>
<div>
<h1>Shopping App</h1>
<ProductList />
<Cart />
</div>
</template>
<script setup>
import ProductList from './ProductList.vue';
import Cart from './Cart.vue';
</script>
商品列表组件 (ProductList.vue)
用户可以在商品列表中添加商品到购物车。
<template>
<div>
<h2>Product List</h2>
<button @click="addToCart('Apple')">Add Apple</button>
<button @click="addToCart('Banana')">Add Banana</button>
</div>
</template>
<script setup>
import { useCartStore } from './stores/cart';
// 使用 Pinia Store
const cartStore = useCartStore();
// 定义一个方法来添加商品到购物车
const addToCart = (item) => {
cartStore.addItem(item);
};
</script>
购物车组件 (Cart.vue)
购物车组件实时展示当前的商品。
<template>
<div>
<h2>Shopping Cart</h2>
<ul>
<li v-for="item in cartItems" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { useCartStore } from './stores/cart';
import { computed } from 'vue';
// 使用 Pinia Store
const cartStore = useCartStore();
// 通过计算属性获取购物车中的商品
const cartItems = computed(() => cartStore.items);
</script>
解释
在这个例子中:
-
定义 Pinia Store:
- 我们创建了一个 Pinia Store
useCartStore
,其中包含一个状态items
来存储购物车中的商品,以及一个动作addItem
来向购物车中添加商品。
- 我们创建了一个 Pinia Store
-
父组件
App.vue
:- 包含两个子组件:
ProductList
和Cart
,分别用于展示商品列表和购物车。
- 包含两个子组件:
-
商品列表组件
ProductList.vue
:- 用户可以点击按钮添加商品到购物车。
- 通过调用
useCartStore
的addItem
方法,将商品添加到购物车中。
-
购物车组件
Cart.vue
:- 实时展示购物车中的商品。
- 通过计算属性
cartItems
读取 Pinia Store 中的items
。
这种方式使用 Pinia 来管理应用的状态,使得兄弟组件之间可以共享状态,并且状态的变化可以自动同步到所有相关组件中,非常适合需要跨组件共享数据的场景。
总结
以上是所有关于组件通信的方法,Vue 3中最好满足单向数据流的原则,遵循单向数据流原则可以带来诸多好处,包括更好的状态管理、更高的可维护性和性能优化。这种设计理念鼓励将数据管理集中化,使组件更加模块化和解耦,从而提高应用程序的整体质量和开发效率。
转载自:https://juejin.cn/post/7394376050372296756