博客 / 詳情

返回

Vue3 組件通信方式

0. 前言

不管是 Vue2 還是 Vue3,組件通信方式很重要,不管是項目還是面試都是經常用到的知識點。

回顧一下 Vue2 中組件的通信方式:

  • props:可以實現父子組件、子父組件、甚至兄弟組件通信
  • 自定義事件:可以實現子父組件通信
  • 全局事件總線 $bus:可以實現任意組件通信
  • pubsub:發佈訂閲模式實現任意組件通信
  • vuex:集中式狀態管理容器,實現任意組件通信
  • ref:父組件獲取子組件實例 VC,獲取子組件的響應式數據以及方法
  • slot:插槽(默認插槽、具名插槽、作用域插槽)實現父子組件通信

示例代碼地址:https://github.com/chenyl8848/vue-technology-stack-study

1. props

props 可以實現父子組件通信,在 Vue3 中可以通過 defineProps 獲取父組件傳遞的數據,且在組件內部不需要引入 defineProps 方法可以直接使用。

父組件給子組件傳遞數據

<template>
  <div class="box">
    <h1>props:我是父組件</h1>
    <Children :money = "money" :info = "info"></Children>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
// 引入子組件
import Children from "./Children.vue";
// 使用 props 實現父子組件通信
let money = ref(1000);
let info = ref('發零花錢了')

</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background-color: pink;
}
</style>

子組件獲取父組件傳遞數據:

<template>
  <div>
    <h2>props:我是子組件</h2>
    <p>接收父組件傳值:{{ props.money }}</p>
    <p>接收父組件傳值:{{ props.info }}</p>
    <!-- 無法修改 -->
    <!-- Set operation on key "money" failed: target is readonly. -->
    <!-- <button @click="updateProps">修改 props 的值</button> -->
  </div>
</template>

<script lang="ts" setup>
// 需要使用到 defineProps 方法去接受父組件傳遞過來的數據
// defineProps是 Vue3 提供方法,不需要引入直接使用
//數組|對象寫法都可以
// let props = defineProps(["money", "info"]);
let props = defineProps({
  money: {
    // 接收數據的類型
    type: Number,
    default: 0,
  },
  info: {
    type: String,
    required: true,
  },
});

// props 是隻讀的,不能修改
// let updateProps = () => {
//   props.money = 3000;
// };
</script>

<style scoped>
</style>
注意:子組件獲取到 props 數據就可以在模板中使用,但是切記 props 是隻讀的(只能讀取,不能修改)。

2. 自定義事件

Vue 框架中事件分為兩種:一種是原生的 DOM 事件,另外一種自定義事件。

原生 DOM 事件可以讓用户與網頁進行交互,比如 clickdbclickchangemouseentermouseleave...

自定義事件可以實現子組件給父組件傳遞數據。

2.1 原生 DOM 事件

代碼如下:

<pre @click="handler1">大江東去,浪淘盡</pre>
<pre @click="handler2(1,2,3,$event)">千古風流人物</pre>
let handler1 = (event) => {
    console.log(event)
}

let handler2 = (x, y, z, $event) => {
    console.log(x, y, z, $event)
}

pre 標籤綁定原生 DOM 事件點擊事件,默認會給事件回調注入 event 事件對象。當點擊事件注入多個參數時,注入的事件對象務叫 $event.

Vue3 框架 clickdbclickchange(這類原生 DOM 事件),不管是在標籤、自定義標籤上(組件標籤)都是原生 DOM 事件。

Vue2 中卻不是這樣的,在 Vue2 中組件標籤需要通過 native 修飾符才能變為原生 DOM 事件。

2.2 自定義事件

自定義事件可以實現子組件給父組件傳遞數據。

在父組件內部給子組件綁定一個自定義事件:

<Children2 @xxx="handler4" @click="handler5"></Children2>

Children2 子組件內部觸發這個自定義事件。

<template>
    <div>
        <h2>自定義事件:我是子組件2</h2>
        <button @click="handler">向父組件傳值,自定義事件xxx</button>
        <br>
        <br>
        <button @click="$emit('click', '321', 'world hello')">向父組件傳值,自定義事件click</button>
    </div>
</template>

<script lang="ts" setup>
// 可以使用 defineEmits 返回函數觸發自定義事件
// defineEmits 方法不需要引入直接使用

let $emit = defineEmits(['xxx', 'click']) 

let handler = () => {
    $emit('xxx', 123, 'hello world')
}
</script>

<style scoped>

</style>

defineEmitsVue3 提供的方法,不需要引入直接使用。defineEmits 方法執行,傳遞一個數組,數組元素即為將來組件需要觸發的自定義事件類型,此方執行會返回一個 $emit 方法用於觸發自定義事件。

當點擊按鈕的時候,事件回調內部調用 $emit 方法去觸發自定義事件,第一個參數為觸發事件類型,第二個、第三個、第N個參數即為傳遞給父組件的數據。

在父組件中接收子組件傳遞過來的參數:

let handler4 = (params1, params2) => {
    console.log(params1, params2)
}

let handler5 = (params1, params2) => {
    console.log(params1, params2)
}

3. 全局事件總線

全局事件總線可以實現任意組件通信,在 Vue2 中可以根據 VMVC 關係推出全局事件總線。

但是在 Vue3 中沒有 Vue 構造函數,也就沒有 Vue.prototype 以及組合式 API 寫法沒有 this,如果想在 Vue3 中使用全局事件總線功能,可以使用插件 mitt 實現。

mitt 官網地址:https://www.npmjs.com/package/mitt

3.1 mitt 安裝

pnpm i mitt

3.2 mitt 定義

// 引入 mitt mitt 是一個方法,方法執行會返回 bus 對象
import mitt from 'mitt';

const $bus = mitt();

export default $bus;

3.3 mitt 使用

mitt 實現全局事件總線,實現兄弟組件之間進行通信:

<template>
  <div class="children2">
    <h2>我是子組件2</h2>
    <button @click="handler">給兄弟組件傳遞值</button>
  </div>
</template>

<script lang="ts" setup>
import $bus from '../../bus'

const handler = () => {
    $bus.emit('car', {car: '蘭博基尼'})
}
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 150px;
  background-color: yellowgreen;
}
</style>
<template>
  <div class="children1">
    <h2>我是子組件1</h2>
  </div>
</template>

<script lang="ts" setup>
import $bus from "../../bus";

// console.log($bus)

// 使用組合式 API 函數
import { onMounted } from "vue";

// 組件掛載完畢的時候,當前組件綁定一個事件,接收將來兄弟組件傳遞的數據
onMounted(() => {
  // 第一個參數即為事件類型 第二個參數即為事件回調
  $bus.on("car", (params) => {
    console.log("接收兄弟組件傳值", params);
  });
});
</script>

<style scoped>
.children1 {
  width: 300px;
  height: 150px;
  background-color: yellow;
}
</style>

4. v-model

v-model 指令可是收集表單數據(數據雙向綁定),除此之外它也可以實現父子組件數據同步。

v-model 實際時基於 props[modelValue] 與自定義事件 [update:modelValue] 實現的。

父組件:

<template>
  <div class="box">
    <h1>我是父組件:v-model</h1>
    <input v-model="info" type="text" />

    <!-- 使用 props 向子組件傳遞數據 -->
    <!-- <Children1 :modelValue="info" @update:modelValue="handler"></Children1> -->

    <!-- 使用 v-model 向子組件傳遞數據 -->
    <!-- 
       在組件上使用 v-model
        第一:相當有給子組件傳遞 props[modelValue]
        第二:相當於給子組件綁定自定義事件update:modelValue
     -->

    <div class="container">
      <Children1 v-model="info"></Children1>

      <Children2
        v-model:pageNo="pageNo"
        v-model:pageSize="pageSize"
      ></Children2>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Children1 from "./Children1.vue";
import Children2 from "./Children2.vue";

let info = ref("");
const handler = (params) => {
  info.value = params;
};

let pageNo = ref(0);
let pageSize = ref(10);
</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
  display: flex;
  justify-content: space-between;
}
</style>

子組件 Children1

<template>
  <div class="children1">
    <h2>我是子組件1</h2>
    <h2>父組件info信息:{{ props.modelValue }}</h2>
    <button @click="handler">同步更新父組件info信息</button>
  </div>
</template>

<script lang="ts" setup>
// 使用defineProps 接收父組件傳值
let props = defineProps(["modelValue"]);
console.log(props);

let $emits = defineEmits(['update:modelValue'])

const handler = () => {
  $emits('update:modelValue', 'hello world')
}

</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

子組件 Children2

<template>
  <div class="children2">
    <h2>我是子組件2</h2>
    <h3>同時綁定多個v-model</h3>
    <button @click="handler">pageNo: {{ pageNo }}</button>
    <br />
    <br />
    <button @click="$emit('update:pageSize', pageSize + 10)">pageSize: {{ pageSize }}</button>
  </div>
</template>

<script lang="ts" setup>

let props = defineProps(['pageNo', 'pageSize'])
let $emit = defineEmits(['update:pageNo', 'update:pageSize'])

const handler = () => {
  $emit('update:pageNo', props.pageNo + 1)
}

</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

<Children1 v-model="info"></Children1> 相當於給組件 Children1 傳遞一個 props(modelValue) 與綁定一個自定義事件 update:modelValue 實現父子組件數據同步。

Vue3 中一個組件可以通過使用多個 v-model,讓父子組件多個數據同步,下方代碼相當於給組件 Children2 傳遞兩個 props 分別是 pageNopageSize,以及綁定兩個自定義事件 update:pageNoupdate:pageSize 實現父子數據同步。

<Children2 v-model:pageNo="pageNo" v-model:pageSize="pageSize"></Children2>

5. useAttrs

Vue3 中可以利用 useAttrs 方法獲取組件的屬性與事件(包含:原生 DOM 事件或者自定義事件),該函數功能類似於 Vue2 框架中 attrs 屬性與 $listeners 方法。

比如:在父組件內部使用一個子組件 HintButton

<template>
  <div class="box">
    <h1>我是父組件:attrs</h1>
    <el-button type="primary" size="large" :icon="Edit"></el-button>
    <!-- 自定義組件 -->
    <!-- 實現將光標放在按鈕上,會有文字提示 -->
    <HintButton type="primary" size="large" :icon="Edit" @click="handler" @xxx="handler" :title="title"></HintButton>
  </div>
</template>

<script lang='ts' setup>
import { Edit } from "@element-plus/icons-vue";
import { ref } from "vue";
import HintButton from "./HintButton.vue";

const handler = () => {
  alert(12306)
}

let title = ref('編輯')

</script>
<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}
</style>

子組件內部可以通過 useAttrs 方法獲取組件屬性與事件。它類似於 props,可以接受父組件傳遞過來的屬性與屬性值。需要注意如果 defineProps 接受了某一個屬性,useAttrs 方法返回的對象身上就沒有相應屬性與屬性值。

<template>
  <div :title="title">
    <el-button :="$attrs"></el-button>
  </div>
</template>

<script lang='ts' setup>
//引入useAttrs方法:獲取組件標籤身上屬性與事件
import { useAttrs } from "vue";
//此方法執行會返回一個對象
let $attrs = useAttrs();

// 萬一用 props 接受 title
let props = defineProps(['title'])
// props 與 useAttrs 方法都可以獲取父組件傳遞過來的屬性與屬性值
//但是 props 接收了 useAttrs 方法就獲取不到了
console.log($attrs)

</script>
<style scoped>
</style>

6. ref$parent

ref 可以獲取元素的 DOM 或者獲取子組件實例的 VC。既然可以在父組件內部通過 ref 獲取子組件實例 VC,那麼子組件內部的方法與響應式數據父組件也是可以使用的。

比如:在父組件掛載完畢獲取組件實例

父組件內部代碼:

<template>
  <div class="box">
    <h1>我是父組件:ref parent</h1>
    <h2>父組件擁有財產:{{ money }}</h2>
    <button @click="handler">向子組件1拿100元</button>
    <div class="container">
        <Children1 ref="children1"></Children1>
        <Children2 ref="children2"></Children2>
    </div>
  </div>
</template>


<script lang='ts' setup>
//ref:可以獲取真實的DOM節點,可以獲取到子組件實例VC
//$parent:可以在子組件內部獲取到父組件的實例
import Children1 from './Children1.vue'
import Children2 from './Children2.vue'

import {ref} from 'vue'

let money = ref(10000)

// 獲取子組件的實例
let children1 = ref()
let children2 = ref()
console.log(children1, children2)

//父組件內部按鈕點擊回調
const handler = () => {
    money.value += 100
    children1.value.money -= 100
}

defineExpose({
    money
})

</script>

<style scoped>

.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
    display: flex;
    justify-content: space-between;
}
</style>

但是需要注意,如果想讓父組件獲取子組件的數據或者方法需要通過 defineExpose 對外暴露,因為 Vue3 中組件內部的數據對外“關閉的”,外部不能訪問。

<template>
  <div class="children1">
    <h2>我是子組件1</h2>
    <h3>子組件1擁有財產:{{ money }}</h3>
  </div>
</template>


<script lang='ts' setup>
import { ref } from "vue";

let money = ref(9999);

defineExpose({
    money
})
</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

$parent 可以獲取某一個組件的父組件實例 VC,因此可以使用父組件內部的數據與方法。必須子組件內部擁有一個按鈕點擊時候獲取父組件實例,當然父組件的數據與方法需要通過 defineExpose 方法對外暴露。

<template>
  <div class="children2">
    <h2>我是子組件2</h2>
    <h3>子組件2擁有財產:{{ money }}</h3>
    <button @click="handler($parent)">向父組件拿300元</button>
  </div>
</template>

<script lang='ts' setup>
import {ref} from 'vue'

let money = ref(1000)

const handler = ($parent) => {
  money.value += 300
  console.log($parent)
  $parent.money -= 300
}
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

7. provideinject

Vue3 提供兩個方法 provide[提供] 與 inject[注入],可以實現隔輩組件傳遞參數。

provide 方法用於提供數據,此方法執需要傳遞兩個參數,分別提供數據的 key 與提供數據 value.

<template>
  <div class="box">
    <h1>我是父組件:provdide-inject</h1>
    <h1>父組件擁有汽車:{{ car }}</h1>
    <Children></Children>
  </div>
</template>


<script lang='ts' setup>
import Children from "./Children.vue";

//vue3 提供 provide(提供)與 inject(注入),可以實現隔輩組件傳遞數據
import { provide, ref } from "vue";
let car = ref("保時捷911");

//祖先組件給後代組件提供數據
//兩個參數:第一個參數就是提供的數據key
//第二個參數:祖先組件提供數據
provide("CAR", car);


</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}
</style>

子組件代碼:

<template>
  <div class="children">
    <h2>我是子組件</h2>
    <Grandson></Grandson>
  </div>
</template>


<script lang='ts' setup>
import Grandson from './Grandson.vue';

</script>

<style scoped>
.children {
  width: 500px;
  height: 250px;
  background: pink;
}
</style>

孫子組件可以通過 inject 方法獲取數據,通過 key 獲取存儲的數值

<template>
  <div class="grandson">
    <h3>我是孫子組件</h3>
    <p>祖先傳下來的汽車:{{ car }}</p>
    <button @click="handler">更換汽車</button>
  </div>
</template>

<script lang='ts' setup>
import { inject } from "vue";

//注入祖先組件提供數據
//需要參數:即為祖先提供數據的 key
let car = inject('CAR')

// 使用 provide-inject 通信可以修改數據
const handler = () => {
  car.value = '自行車'
}

</script>

<style scoped>
.grandson {
  width: 200px;
  height: 200px;
  background: hotpink;
}
</style>

8. pinia

pinia 官網:https://pinia.web3doc.top/

pinia 也是集中式管理狀態容器,類似於 Vuex.但是核心概念沒有 mutationmodules.

8.1 pinia 安裝

pnpm i pinia

8.2 pinia 註冊

創建 store

import { createPinia } from 'pinia';

let store = createPinia()

export default store;

main.ts 中註冊使用:

import { createApp } from 'vue'

import store from './store'

import App from './App.vue'

const app = createApp(App)

app.use(store)

app.mount('#app')

8.3 pinia 使用

選項式API使用:

import { defineStore } from 'pinia'

//第一個參數:小倉庫名字  第二個參數:小倉庫配置對象
//defineStore 方法執行會返回一個函數,函數作用就是讓組件可以獲取到倉庫數據
let useInfoStore = defineStore("info", {
    // 注意寫法與 vue2 中 的 vuex 寫法不同

    // state 存儲數據
    state: () => {
        return {
            count: 999,
            arr: [1, 2, 3, 4, 5, 6, 7, 8, 9]
        }
    },
    // 對數據進行操作
    actions: {
        //注意:函數沒有context上下文對象
        //沒有commit、沒有mutations去修改數據
        updateCount(param1: number, param2: number) {
            this.count = param1 + param2
        }
    },
    // 獲取數據
    getters: {
        total() {
            let result:number = this.arr.reduce((prev, next) => {
                return prev + next
            }, 0)
            return result
        }
    }
})

export default useInfoStore;

在組件中使用:

<template>
  <div class="children1">
    <h2>我是子組件1</h2>
    <h3>count:{{ infoStore.count }}</h3>
    <button @click="handler">修改infoStore中的數據</button>
    <h3>total:{{ infoStore.total }}</h3>
  </div>
</template>


<script lang='ts' setup>
import useInfoStore from "../../store/module/info";

// 獲取小倉庫對象
let infoStore = useInfoStore()

const handler = () => {
  //倉庫調用自身的方法去修改倉庫的數據
  infoStore.updateCount(99, 100)
}

</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

組合式 API 使用:

import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

let useTodoStore = defineStore('todo', () => {
    let todoList = ref([
        { id: 1, title: '吃飯', done: true },
        { id: 2, title: '睡覺', done: false },
        { id: 3, title: '打遊戲', done: true }
    ])

    let arr = ref([1, 2, 3, 4, 5])

    const total:any = computed(() => {
        return arr.value.reduce((prev, next) => {
            return prev + next
        }, 0)
    })

    const updateTodo = (params: any) => {
        todoList.value.unshift(params)
    }

    // 務必要返回一個對象:屬性與方法可以提供給組件使用
    return {
        todoList,
        total,
        updateTodo
    }
})

export default useTodoStore;

在組件中使用:

<template>
  <div class="children2">
    <h2>我是子組件2</h2>
    <ul>
      <li v-for="(item, index) in todoStore.todoList" :key="item.id">{{ item.title }}</li>
    </ul>
    <button @click="handler">修改todo</button>
    <h3>totalCount:{{ todoStore.total }}</h3>
  </div>
</template>

<script lang='ts' setup>
import useTodoStore from '../../store/module/todo'

let todoStore = useTodoStore()

const handler = () => {
  let params = { id: 4, title: '學習', done: true }
  todoStore.updateTodo(params)
}
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

9. slot

插槽:默認插槽、具名插槽、作用域插槽可以實現父子組件通信。

9.1 默認插槽

在子組件內部的模板中書寫 slot 全局組件標籤

<template>
  <div class="children1">
    <h2>我是子組件1</h2>
    <!-- 默認插槽-->
    <slot></slot>
  </div>
</template>

<script lang='ts' setup>

</script>

<style scoped>

</style>

在父組件內部提供結構,Children1 即為子組件,在父組件內部使用的時候,在雙標籤內部書寫結構傳遞給子組件。

<template>
  <div class="box">
    <h1>我是父組件:slot</h1>
    <div class="container">
      <Children1>
        <span>默認插槽</span>
      </Children1>
    </div>
  </div>
</template>

<script lang='ts' setup>
import Children1 from "./Children1.vue";

</script>

<style scoped>

</style>
注意開發項目的時候默認插槽一般只有一個。

9.2 具名插槽

顧名思義,此插槽帶有名字,在組件內部可以有多個指定名字的插槽。

下面是一個子組件內部,模板中有兩個插槽:

<template>
  <div class="children1">
    <h2>我是子組件1</h2>
    <!-- 默認插槽-->
    <slot></slot>
    <slot name="a"></slot>
    <slot name="b"></slot>
  </div>
</template>

<script lang='ts' setup>

</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

父組件內部向指定的具名插槽傳遞結構,v-slot 可以替換為 #

<template>
  <div class="box">
    <h1>我是父組件:slot</h1>
    <div class="container">
      <Children1>
        <span>默認插槽</span>
      </Children1>
      <Children1>
        <template v-slot:a>
          <span>具名插槽a</span>
        </template>
      </Children1>
      <Children1>
        <template #b>
          <span>具名插槽b</span>
        </template>
      </Children1>
    </div>
  </div>
</template>


<script lang='ts' setup>
import Children1 from "./Children1.vue";

</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
  display: flex;
  justify-content: space-between;
}
</style>

9.3 作用域插槽

作用域插槽:子組件數據由父組件提供,但是子組件內部決定不了自身結構與外觀(樣式)

子組件 Children2 代碼如下:

<template>
  <div class="children2">
    <h2>我是子組件2:作用域插槽</h2>
    <ul>
      <li v-for="(item, index) in todos" :key="item.id">
        <!--作用域插槽:可以講數據回傳給父組件-->
        <slot :$row="item" :$index="index"></slot>
      </li>
    </ul>
  </div>
</template>

<script lang='ts' setup>
//通過props接受父組件傳遞數據
defineProps(["todos"]);
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

父組件內部代碼如下:

<template>
  <div class="box">
    <h1>我是父組件:slot</h1>
    <div class="container">
      <Children1>
        <span>默認插槽</span>
      </Children1>
      <Children1>
        <template v-slot:a>
          <span>具名插槽a</span>
        </template>
      </Children1>
      <Children1>
        <template #b>
          <span>具名插槽b</span>
        </template>
      </Children1>
      <Children2 :todos="todos">
        <template v-slot="{ $row, $index }">
          <p :style="{ color: $row.done ? 'green' : 'red' }">
            {{ $row.title }}
          </p>
        </template>
      </Children2>
    </div>
  </div>
</template>


<script lang='ts' setup>
import Children1 from "./Children1.vue";
import Children2 from "./Children2.vue";

//插槽:默認插槽、具名插槽、作用域插槽
//作用域插槽:就是可以傳遞數據的插槽,子組件可以將數據回傳給父組件,父組件可以決定這些回傳的數據是以何種結構或者外觀在子組件內部去展示!!!

import { ref } from "vue";
//todos數據
let todos = ref([
  { id: 1, title: "吃飯", done: true },
  { id: 2, title: "睡覺", done: false },
  { id: 3, title: "打遊戲", done: true },
  { id: 4, title: "學習", done: false },
]);
</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
  display: flex;
  justify-content: space-between;
}
</style>
user avatar ixuea 頭像 zhouyanlin 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.