likes
comments
collection
share

所有组件通信的方法,一次学个够

作者站长头像
站长
· 阅读数 19

前言

在现代前端开发中,组件化已经成为构建复杂用户界面的主要方法。组件化设计使得应用程序可以被拆分成小的、可复用的模块,每个模块(组件)���以独立开发、测试和维护。然而,在实际开发中,不同组件之间的通信成为了一个重要且不可避免的问题。组件通信的有效管理对于应用的正确性、可维护性和扩展性至关重要。

组件通信的常见方法

  1. Props 和 Events

    • 使用 props 传递数据,使用事件(如 $emit)进行反馈,是最常见的父子组件通信方式。
  2. Provide 和 Inject

    • 用于跨层级通信,父组件使用 provide 提供数据或方法,子组件通过 inject 注入。
  3. Ref 和 DefineExpose

    • 通过 ref 获取子组件实例,并结合 defineExpose 暴露方法或数据。
  4. 全局状态管理工具

    • 使用 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>

解释

在这个例子中:

  1. 父组件 ParentComponent.vue:

    • 使用 ref 定义响应式变量 messageFromChild,用于存储从子组件接收到的消息。
    • 定义 handleTaskCompleted 方法,当子组件触发 task-completed 事件时调用此方法,并更新 messageFromChild 的值。
  2. 子组件 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>

解释

在这个例子中:

  1. 父组件 ParentComponent.vue:

    • 使用 ref 定义响应式变量 taskStatus,初始值为 '未完成'
    • 使用 v-model:status 绑定 taskStatus 变量到子组件的 status 属性。
    • 当子组件通过 update:status 事件更新任务状态时,父组件会自动更新 taskStatus 的值。
  2. 子组件 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>

解释

在这个例子中:

  1. 父组件 ParentComponent.vue:

    • 使用 ref 绑定子组件实例。
    • 通过 childComponentRef 获取子组件实例,并调用其暴露的方法 getTaskStatus
    • 将获取到的任务状态存储在响应式变量 taskStatus 中。
  2. 子组件 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>

解释

在这个例子中:

  1. 父组件 ParentComponent.vue:

    • 使用 provide 提供一个名为 food 的值,代表准备好的食物。
    • provide('food', food) 将食物传递给子组件。
  2. 子组件 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>

解释

在这个例子中:

  1. 定义 Pinia Store:

    • 我们创建了一个 Pinia Store useCartStore,其中包含一个状态 items 来存储购物车中的商品,以及一个动作 addItem 来向购物车中添加商品。
  2. 父组件 App.vue:

    • 包含两个子组件:ProductListCart,分别用于展示商品列表和购物车。
  3. 商品列表组件 ProductList.vue:

    • 用户可以点击按钮添加商品到购物车。
    • 通过调用 useCartStoreaddItem 方法,将商品添加到购物车中。
  4. 购物车组件 Cart.vue:

    • 实时展示购物车中的商品。
    • 通过计算属性 cartItems 读取 Pinia Store 中的 items

这种方式使用 Pinia 来管理应用的状态,使得兄弟组件之间可以共享状态,并且状态的变化可以自动同步到所有相关组件中,非常适合需要跨组件共享数据的场景。

总结

以上是所有关于组件通信的方法,Vue 3中最好满足单向数据流的原则,遵循单向数据流原则可以带来诸多好处,包括更好的状态管理、更高的可维护性和性能优化。这种设计理念鼓励将数据管理集中化,使组件更加模块化和解耦,从而提高应用程序的整体质量和开发效率。

转载自:https://juejin.cn/post/7394376050372296756
评论
请登录