饮冰十年-人工智能-Vue3-67-组件间数据交互

上一篇:饮冰三年-人工智能-Vue-66 Vue组件化

  很久以前我对Vue2的组件间数据交互做过学习,兜兜转转再用Vue已经是Vue3版本。

Vue3组件间数据交互

1、准备工作

  环境准备

    使用 Vite 创建一个新的 Vue 3 项目

  功能介绍

    该功能由APPVue+4个组件组成  
  •     头部组件(MyHeader)

        主要是一个input框,用于收集用户输入内容,

        当用户输入完成,并按下enter键后,将数据添加到 MyList 列表组件中

  •     列表组件(MyList)
        主要用于展示待办项
  •     任务组件(MyItem)
        包括 左边勾选框、中间任务内容、右边删除按钮
        点击勾选框,标记任务是否完成;点击删除按钮,删除任务项
  •     底部组件(MyFooter)
        用于显示待办事项列表的统计信息,并提供全选和清除已完成任务的功能。

2、props emit 版本

<template>
  <div class="todo-header">
    <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add" />
  </div>
</template>

<script setup>
// 引入nanoid
import { nanoid } from "nanoid";
// 定义 emits
const emit = defineEmits(['addTodo'])
const add = (e) => {
  // 判断用户是否输入了内容
  if (e.target.value.trim().length === 0) {
    alert("输入的内容不能为空");
    return;
  }
  // 将用户的输入,包装成为一个todo对象
  const todoObj = {
    id: nanoid(),
    title: e.target.value,
    done: false,
  };
  // 将todo对象传递给App组件
  emit('addTodo', todoObj)
  // 清空用户的输入
  e.target.value = "";
}

</script>

<style scoped>
/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
    0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
MyHeader

源码解析

  emit 是一个用于触发事件的方法,允许子组件向父组件发送消息。这是 Vue 的一种组件通信机制,通常用于子组件向父组件传递数据或通知父组件发生了某个事件。

作用和用法

  • 事件传递: 当子组件需要通知父组件某个事件发生时,可以使用 emit 方法。例如,当用户在输入框中输入任务并按下回车键时,你想要将这个任务传递给父组件。

  • 自定义事件: emit 允许你创建自定义事件,父组件可以通过监听这些事件来响应子组件的行为。例如,在示例中,子组件使用 emit('addTodo', todoObj) 来触发名为 addTodo 的事件,并将 todoObj 作为参数传递给父组件。

如何在父组件中监听事件

  • 在父组件中,你可以通过 @ 符号来监听子组件发出的事件

  • <MyHeader @addTodo="addTodo"></MyHeader>

<template>
  <ul class="todo-main">
    <!-- v-for 指令用于遍历 todos 数组,生成多个 MyItem 组件。
    :todo="todoObj": 将当前的 todoObj 传递给 MyItem 组件的 todo prop。
    :checkTodo="checkTodo" 和 :deleteTodo="deleteTodo": 将父组件的方法 checkTodo 和 deleteTodo 作为 prop 传递给 MyItem 组件,允许子组件在适当的时候调用这些方法 -->
    <MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :checkTodo="checkTodo" :deleteTodo="deleteTodo">
    </MyItem>
  </ul>
</template>

<script setup>
 
import MyItem from './MyItem.vue';
// defineProps 是一个 Vue 3 的编译宏,用于在 <script setup> 中定义组件接收的 props。
const props = defineProps({
  todos: Array, // todos: 一个数组,用于存储待办事项列表。
  checkTodo: Function, // checkTodo: 一个函数,用于处理勾选或取消勾选待办事项的操作。
  deleteTodo: Function // deleteTodo: 一个函数,用于处理删除待办事项的操作。
});
</script>

<style scoped>
/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>
MyList.Vue

源码解析

  • 父组件通过 props 向子组件传递数据和方法。

作用和用法

  • 父组件传:父组件 APPVue 通过 : 符号绑定 todos、checkTodo 和 deleteTodo到子组件 MyList 组件。 Vue 会将这些数据和方法作为 props 传递给 MyList 组件。
<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></MyList>
  • 子组件接:在 MyList.vue 组件中,使用 defineProps 来定义和接收这些 props:

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)" />
      <span>{{ todo.title }}</span>
    </label>
    <button class="btn btn-danger" @click="handleTodo(todo.id)">删除</button>
  </li>
</template>

<script setup>
 

// 接收 props
const props = defineProps({
  todo: Object,
  checkTodo: Function,
  deleteTodo: Function
})
// 处理勾选事件
const handleCheck = (id) => {
  // 通知App组件,修改todo的done状态
  props.checkTodo(id)
}

// 处理删除事件
const handleTodo = (id) => {
  if (window.confirm('确定删除吗?')) {
    props.deleteTodo(id)
  }
}     
</script>

<style scoped>
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

li:hover {
  background-color: #ddd;
}

li:hover button {
  display: block;
}
</style>
MyItem.vue

源码解析

  有了上面的基础,这个比较好理解,父组件通过 props 向子组件传递数据和方法。不过不同的是,子组件在这里调用了props传的方法。

// 处理勾选事件
const handleCheck = (id) => {
  // 通知App组件,修改todo的done状态
  props.checkTodo(id)
}

// 处理删除事件
const handleTodo = (id) => {
  if (window.confirm('确定删除吗?')) {
    props.deleteTodo(id)
  }
}  
<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" :checked="isAll" @change="checkAll" />
    </label>
    <span> <span>已完成{{ doneTotal }}</span> / 全部{{ todos.length }} </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script setup>
import { computed} from 'vue'

// 接收 props
const props = defineProps({
  todos: {
    type: Array,
    required: true
  }
});
// 定义 emits
const emit = defineEmits(['checkAllTodo', 'clearAllTodo']);

// 计算属性
const total = computed(() => props.todos.length)

const doneTotal = computed(() => {
  return props.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
})

const isAll = computed(() => doneTotal.value === total.value && total.value > 0)

const checkAll = (e) => {
  emit('checkAllTodo', e.target.checked);
}

const clearAll = () => {
  emit('clearAllTodo')
}

</script>

<style scoped>
/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}
</style>
MyFooter.vue

源码解析

  • 定义 props:

    • props 接收一个 todos 数组,表示所有的待办事项。

    • defineProps的时候可以为属性设置必填项

  • 定义 emits:
    • emit 用于向父组件发送事件。定义了两个事件:checkAllTodo 和 clearAllTodo。

    • checkAll: 当复选框状态改变时,触发 checkAllTodo 事件,并传递复选框的勾选状态。

    • clearAll: 当点击清除按钮时,触发 clearAllTodo 事件。

<script setup>
import { ref,  watch } from 'vue'
import MyHeader from './components/MyHeader.vue'
import MyList from './components/MyList .vue'
import MyFooter from './components/MyFooter.vue'
// 初始化 todos
const todos = ref(JSON.parse(localStorage.getItem('todos')) || [])
// 添加todo
const addTodo = (todo) => {
  todos.value.unshift(todo);
};


// 勾选或者取消勾选一个todo
const checkTodo = (id) => {
  todos.value.forEach((todo) => {
    if (todo.id === id) {
      todo.done = !todo.done;
    }
  });
};
// 删除一个todo
const deleteTodo = (id) => {
  todos.value = todos.value.filter((todo) => todo.id !== id);
};

// 全选或者全不选
const checkAllTodo = (done) => {
  todos.value.forEach(todo => todo.done = done)
};

// 清除所有已经完成的todo
const clearAllTodo = () => {
  todos.value = todos.value.filter((todo) => !todo.done)
}
// 监听 todos 的变化并同步到 localStorage
watch(
  todos,
  (newValue) => {
    localStorage.setItem('todos', JSON.stringify(newValue));
  },
  { deep: true }
);
</script>

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 在父组件中,你可以通过 @ 符号来监听子组件发出的事件 -->
        <MyHeader @addTodo="addTodo"></MyHeader>
        <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></MyList>
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"></MyFooter>
      </div>
    </div>
  </div>
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}

.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}

.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
APP.vue
这段源码大家可自行理解下。

总结

数据流: Vue 中的 props 实现了单向数据流,父组件通过 props 将数据和方法传递给子组件,子组件通过 emit 发送事件回父组件
理解子组件中使用 emit 和直接通过 props 调用父组件方法的差异,可以通过一个简单的生活类比来解释。
类比:点餐和送餐
假设你在餐厅工作,负责点餐和送餐。餐厅有两个主要角色:服务员(父组件)和厨房(子组件)。
1. 使用 emit(服务员通知厨房)
    服务员(父组件):
        服务员记录顾客的点单(父组件的方法)。
        服务员将点单传递给厨房(通过 emit 将事件传递给父组件)。
    厨房(子组件):
        厨房收到点单后,准备菜肴(子组件执行操作)。
        厨房准备好菜肴后,通过铃声(emit 事件)通知服务员。
场景:
    顾客点了一道菜,服务员记录并传递给厨房。
    厨房准备好菜肴后,通过铃声通知服务员。
    服务员听到铃声后,将菜肴送到顾客桌上。
解释:
    服务员通过监听厨房的铃声(emit 事件)来知道菜肴已准备好,并将菜送到顾客桌上。
    服务员没有直接去厨房检查菜肴是否准备好(不直接调用父组件的方法)。
2. 直接通过 props 调用(服务员自己做菜)
    服务员(父组件):
        服务员记录顾客的点单(父组件的方法)。
        服务员直接自己准备菜肴(通过 props 直接调用父组件的方法)。
    厨房(子组件):
        厨房只是一个摆设(子组件只是传递数据),所有操作都由服务员完成。
场景:
    顾客点了一道菜,服务员记录并自己准备菜肴。
    服务员准备好菜肴后,直接将菜肴送到顾客桌上。
解释:
    服务员没有依赖厨房的通知(emit 事件),而是自己完成所有步骤。
    服务员直接根据点单准备菜肴(直接调用父组件的方法)。
对比
    emit:像厨房通过铃声(事件)通知服务员菜肴已准备好。厨房不直接送菜,通知服务员由服务员完成最终的送菜动作。适用于子组件只需通知父组件某个事件发生,而不需要知道具体如何处理。
    直接调用 props 方法:像服务员自己准备菜肴,不依赖厨房的通知。服务员直接完成点单记录和菜肴准备的所有步骤。适用于子组件需要直接调用父组件的方法来完成某些操作。
 

2、全局事件总线版本

 
    全局事件总线(Global Event Bus)全局事件总线是一个可以在应用的任何部分触发和监听事件的机制。
    适用场景
        它可以简化父子组件之间的通信,特别是当组件层级很深或者组件之间没有直接的父子关系时。
    使用方法
        在 Vue 3 中,不再推荐使用类似 Vue 2 中的 $bus 方式来创建全局事件总线
        在 Vue 3 中,我们可以使用 mitt 这个库来创建一个简单的全局事件总线。mitt 是一个轻量级的事件总线库

安装

npm install mitt

添加时间总线eventBus.js

import mitt from 'mitt';

const eventBus = mitt();

export default eventBus;
eventBus.js

MyHeader、MyFooter 这两个源码保持不变

<template>
  <ul class="todo-main">
    <!-- eventBus02  <MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :checkTodo="checkTodo" :deleteTodo="deleteTodo"> -->
    <MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj">
    </MyItem>
  </ul>
</template>

<script setup>

import MyItem from './MyItem.vue';
// defineProps 是一个 Vue 3 的编译宏,用于在 <script setup> 中定义组件接收的 props。
const props = defineProps({
  todos: Array, // todos: 一个数组,用于存储待办事项列表。
  // checkTodo: Function, //  eventBus02 注释掉一些
  // deleteTodo: Function //  eventBus02 注释掉一些
});
</script>

<style scoped>
/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>
MyList.Vue

源码解析

  • 去掉props传递方式

  • 不做中间商

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)" />
      <span>{{ todo.title }}</span>
    </label>
    <button class="btn btn-danger" @click="handleTodo(todo.id)">删除</button>
  </li>
</template>

<script setup>
import eventBus from '../eventBus';  // eventBus01 导入事件总线

// 接收 props
const props = defineProps({
  todo: Object,
  // checkTodo: Function, eventBus02 注释掉一些
  // deleteTodo: Function
})
// 处理勾选事件
const handleCheck = (id) => {
  // eventBus03 通知父组件,修改 todo 的 done 状态
  eventBus.emit('checkTodo', id);
}

// 处理删除事件
const handleTodo = (id) => {
  if (window.confirm('确定删除吗??')) {
    // eventBus04 通知父组件,
    eventBus.emit('deleteTodo', id);
  }
}     
</script>

<style scoped>
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

li:hover {
  background-color: #ddd;
}

li:hover button {
  display: block;
}
</style>
MyItem.vue

源码解析

  •  // eventBus01 导入事件总线

    • import eventBus from '../eventBus';

  • // eventBus02 注释掉一些

    • // 接收 props const props = defineProps({ todo: Object, // checkTodo: Function, // deleteTodo: Function })

  • // eventBus 通知父组件

    • // 处理勾选事件 const handleCheck = (id) => { // eventBus03 通知父组件,修改 todo 的 done 状态 eventBus.emit('checkTodo', id); } // 处理删除事件 const handleTodo = (id) => { if (window.confirm('确定删除吗??')) { eventBus.emit('deleteTodo', id); } }    

<script setup>
import { ref, watch } from 'vue'
import eventBus from './eventBus';  // eventBus01 导入事件总线
import MyHeader from './components/MyHeader.vue'
import MyList from './components/MyList .vue'
import MyFooter from './components/MyFooter.vue'
// 初始化 todos
const todos = ref(JSON.parse(localStorage.getItem('todos')) || [])
// 添加todo
const addTodo = (todo) => {
  todos.value.unshift(todo);
};


// 勾选或者取消勾选一个todo
const checkTodo = (id) => {
  todos.value.forEach((todo) => {
    if (todo.id === id) {
      todo.done = !todo.done;
    }
  });
};
// 删除一个todo
const deleteTodo = (id) => {
  todos.value = todos.value.filter((todo) => todo.id !== id);
};

// 全选或者全不选
const checkAllTodo = (done) => {
  todos.value.forEach(todo => todo.done = done)
};

// 清除所有已经完成的todo
const clearAllTodo = () => {
  todos.value = todos.value.filter((todo) => !todo.done)
}
// 监听 todos 的变化并同步到 localStorage
watch(
  todos,
  (newValue) => {
    localStorage.setItem('todos', JSON.stringify(newValue));
  },
  { deep: true }
);

// eventBus03 将方法注册到事件总线上
eventBus.on('checkTodo', checkTodo);
eventBus.on('deleteTodo', deleteTodo);
</script>

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 在父组件中,你可以通过 @ 符号来监听子组件发出的事件 -->
        <MyHeader @addTodo="addTodo"></MyHeader>
        <!-- eventBus02  <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></MyList> -->
        <MyList :todos="todos"></MyList>
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"></MyFooter>
      </div>
    </div>
  </div>
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}

.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}

.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
APPVue.Vue

源码解析

  • // eventBus01 导入事件总线

    • import eventBus from './eventBus';

  • // eventBus02 修改原来props传递方式

    • <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></MyList>

    • <MyList :todos="todos"></MyList>

  • // eventBus03 将方法注册到事件总线上

    • eventBus.on('checkTodo', checkTodo); eventBus.on('deleteTodo', deleteTodo);

2、发布订阅模式

    在 Vue 3 中,全局事件总线和发布-订阅模式是相似的概念,实际上事件总线就是一种实现发布-订阅模式的方式。
    发布-订阅模式:
        这是一种设计模式,允许对象之间的解耦。发布者(发布事件的组件)和订阅者(监听事件的组件)之间不直接交互,而是通过一个中介(事件总线)来通信。
        发布者发布消息,订阅者通过监听特定的事件来响应这些消息。
    事件总线:
        事件总线是实现发布-订阅模式的一种具体方式。在 Vue 中,事件总线通常是一个独立的对象,它提供了 on(订阅事件)、emit(发布事件)和 off(取消订阅)等方法。
        使用事件总线,组件可以轻松地发布和订阅事件,而无需了解彼此的存在。
    总结
        在 Vue 3 中,通过创建一个事件总线(如使用 mitt 库)实现组件之间的消息传递,实际上就是在应用发布-订阅模式。这使得组件之间的通信更加灵活和解耦,便于维护和扩展。
posted @ 2024-07-31 19:30  逍遥小天狼  阅读(18)  评论(0编辑  收藏  举报