飲冰十年-人工智慧-Vue3-67-元件間資料互動

逍遥小天狼發表於2024-07-31

上一篇:飲冰三年-人工智慧-Vue-66 Vue元件化

  很久以前我對Vue2的元件間資料互動做過學習,兜兜轉轉再用Vue已經是Vue3版本。

Vue3元件間資料互動

1、準備工作

  環境準備

    使用 Vite 建立一個新的 Vue 3 專案

  功能介紹

    該功能由APPVue+4個元件組成
  • 頭部元件(MyHeader)

主要是一個input框,用於收集使用者輸入內容,

當使用者輸入完成,並按下enter鍵後,將資料新增到 MyList 列表元件中

  • 列表元件(MyList)
主要用於展示待辦項
  • 任務元件(MyItem)
包括 左邊勾選框、中間任務內容、右邊刪除按鈕
點選勾選框,標記任務是否完成;點選刪除按鈕,刪除任務項
  • 底部元件(MyFooter)
用於顯示待辦事項列表的統計資訊,並提供全選和清除已完成任務的功能。

2、props emit 版本

飲冰十年-人工智慧-Vue3-67-元件間資料互動
<template>
  <div class="todo-header">
    <input type="text" placeholder="請輸入你的任務名稱,按Enter鍵確認" @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 方法。例如,當使用者在輸入框中輸入任務並按下Enter鍵時,你想要將這個任務傳遞給父元件。

  • 自定義事件: emit 允許你建立自定義事件,父元件可以透過監聽這些事件來響應子元件的行為。例如,在示例中,子元件使用 emit('addTodo', todoObj) 來觸發名為 addTodo 的事件,並將 todoObj 作為引數傳遞給父元件。

如何在父元件中監聽事件

  • 在父元件中,你可以透過 @ 符號來監聽子元件發出的事件

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

飲冰十年-人工智慧-Vue3-67-元件間資料互動
<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:

飲冰十年-人工智慧-Vue3-67-元件間資料互動
<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)
  }
}  
飲冰十年-人工智慧-Vue3-67-元件間資料互動
<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 事件。

飲冰十年-人工智慧-Vue3-67-元件間資料互動
<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

飲冰十年-人工智慧-Vue3-67-元件間資料互動
import mitt from 'mitt';

const eventBus = mitt();

export default eventBus;
eventBus.js

MyHeader、MyFooter 這兩個原始碼保持不變

飲冰十年-人工智慧-Vue3-67-元件間資料互動
<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傳遞方式

  • 不做中間商

飲冰十年-人工智慧-Vue3-67-元件間資料互動
<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); } }

飲冰十年-人工智慧-Vue3-67-元件間資料互動
<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 庫)實現元件之間的訊息傳遞,實際上就是在應用釋出-訂閱模式。這使得元件之間的通訊更加靈活和解耦,便於維護和擴充套件。

相關文章