Vue3的組件通訊的十種方式

前言

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

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

  • props:可以實現(xiàn)父子組件、子父組件、甚至兄弟組件通信
  • 自定義事件:可以實現(xiàn)子父組件通信
  • 全局事件總線 $bus:可以實現(xiàn)任意組件通信
  • pubsub:發(fā)布訂閱模式實現(xiàn)任意組件通信
  • vuex:集中式狀態(tài)管理容器,實現(xiàn)任意組件通信
  • ref:父組件獲取子組件實例 VC,獲取子組件的響應(yīng)式數(shù)據(jù)以及方法
  • slot:插槽(默認(rèn)插槽、具名插槽、作用域插槽)實現(xiàn)父子組件通信

示例代碼地址:https://github.com/chenyl8848/v

一、props

props 可以實現(xiàn)父子組件通信,在 Vue3 中可以通過 defineProps獲取父組件傳遞的數(shù)據(jù),且在組件內(nèi)部不需要引入 defineProps 方法可以直接使用。

1.1. 父組件給子組件傳遞數(shù)據(jù)

<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 實現(xiàn)父子組件通信
let money = ref(1000);
let info = ref("發(fā)零花錢了");
</script>

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

1.2. 子組件獲取父組件傳遞數(shù)據(jù):

注意

  • 需要使用到 defineProps 方法去接受父組件傳遞過來的數(shù)據(jù)
  • definePropsVue3 提供方法,不需要引入直接使用
  • 數(shù)組|對象寫法都可以
  • props只讀的,不能修改
<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 方法去接受父組件傳遞過來的數(shù)據(jù)
// defineProps是 Vue3 提供方法,不需要引入直接使用
//數(shù)組|對象寫法都可以
// let props = defineProps(["money", "info"]);
let props = defineProps({
  money: {
    // 接收數(shù)據(jù)的類型
    type: Number,
    default: 0,
  },
  info: {
    type: String,
    required: true,
  },
});

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

<style scoped></style>

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

二、自定義事件

在 Vue 框架中事件分為兩種:

  • 一種是原生的 DOM 事件
    原生 DOM 事件可以讓用戶與網(wǎng)頁進行交互,比如 click、 dbclick、 change、 mouseenter、 mouseleave...
  • 另外一種自定義事件。
    自定義事件可以實現(xiàn)子組件給父組件傳遞數(shù)據(jù)。

2.1. 原生 DOM 事件

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

let handler2 = (x, y, z, $event) => {
    console.log(x, y, z, $event)
}
  • pre 標(biāo)簽綁定原生 DOM 事件點擊事件,默認(rèn)會給事件回調(diào)注入 event 事件對象。當(dāng)點擊事件注入多個參數(shù)時,注入的事件對象務(wù)叫 $event.
  • Vue3 框架 click、 dbclick、 change(這類原生 DOM 事件),不管是在標(biāo)簽、自定義標(biāo)簽上(組件標(biāo)簽)都是原生 DOM 事件
  • Vue2 中卻不是這樣的,在 Vue2 中組件標(biāo)簽需要通過 native 修飾符才能變?yōu)樵?DOM 事件。

2.2. 自定義事件

自定義事件可以實現(xiàn)子組件給父組件傳遞數(shù)據(jù)。

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

當(dāng)點擊按鈕的時候,事件回調(diào)內(nèi)部調(diào)用 $emit 方法去觸發(fā)自定義事件,第一個參數(shù)為觸發(fā)事件類型,第二個、第三個、第N個參數(shù)即為傳遞給父組件的數(shù)據(jù)。

  1. 在父組件內(nèi)部給子組件綁定一個自定義事件:
<Children2 @xxx="handler4" @click="handler5"></Children2>
  1. 在 Children2 子組件內(nèi)部觸發(fā)這個自定義事件。
<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 返回函數(shù)觸發(fā)自定義事件
// defineEmits 方法不需要引入直接使用

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

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

<style scoped></style>
  1. 在父組件中接收子組件傳遞過來的參數(shù):
let handler4 = (params1, params2) => {
    console.log(params1, params2)
}

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

三、全局事件總線

全局事件總線可以實現(xiàn)任意組件通信,在 Vue2 中可以根據(jù) VMVC 關(guān)系推出全局事件總線。

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

mitt 官網(wǎng)地址:https://www.npmjs.com/package/mitt

3.1 mitt 安裝

npm i mitt

3.2 mitt 定義

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

const $bus = mitt();

export default $bus;

3.3 mitt 使用

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

<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 函數(shù)
import { onMounted } from "vue";

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

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

四、v-model

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

v-model 實際時基于 props[modelValue] 與自定義事件 [update:modelValue]實現(xiàn)的。

4.1. 父組件:

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

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

    <!-- 使用 v-model 向子組件傳遞數(shù)據(jù) -->
    <!-- 
       在組件上使用 v-model
        第一:相當(dāng)有給子組件傳遞 props[modelValue]
        第二:相當(dāng)于給子組件綁定自定義事件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>

4.2、子組件 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>

4.3. 子組件 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> 相當(dāng)于給組件 Children1 傳遞一個 props(modelValue) 與綁定一個自定義事件 update:modelValue 實現(xiàn)父子組件數(shù)據(jù)同步。

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

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

五、useAttrs

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

比如:在父組件內(nèi)部使用一個子組件 HintButton

<template>
  <div class="box">
    <h1>我是父組件:attrs</h1>
    <el-button type="primary" size="large" :icon="Edit"></el-button>
    <!-- 自定義組件 -->
    <!-- 實現(xiàn)將光標(biāo)放在按鈕上,會有文字提示 -->
    <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>

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

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

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

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

六、ref 與 $parent

ref 可以獲取元素的 DOM 或者獲取子組件實例的 VC。既然可以在父組件內(nèi)部通過 ref 獲取子組件實例 VC,那么子組件內(nèi)部的方法與響應(yīng)式數(shù)據(jù)父組件也是可以使用的。

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

6.1、父組件內(nèi)部代碼:

<template>
  <div class="box">
    <h1>我是父組件:ref parent</h1>
    <h2>父組件擁有財產(chǎn):{{ 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節(jié)點,可以獲取到子組件實例VC
//$parent:可以在子組件內(nèi)部獲取到父組件的實例
import Children1 from "./Children1.vue";
import Children2 from "./Children2.vue";

import { ref } from "vue";

let money = ref(10000);

// 獲取子組件的實例 變量名就是ref綁定的名字,切記!
let children1 = ref();
let children2 = ref();
console.log(children1, children2);

//父組件內(nèi)部按鈕點擊回調(diào)
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>

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

<template>
  <div class="children1">
    <h2>我是子組件1</h2>
    <h3>子組件1擁有財產(chǎn):{{ 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,因此可以使用父組件內(nèi)部的數(shù)據(jù)與方法。必須子組件內(nèi)部擁有一個按鈕點擊時候獲取父組件實例,當(dāng)然父組件的數(shù)據(jù)與方法需要通過 defineExpose 方法對外暴露。

<template>
  <div class="children2">
    <h2>我是子組件2</h2>
    <h3>子組件2擁有財產(chǎn):{{ 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>

七、provide 與 inject

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

provide 方法用于提供數(shù)據(jù),此方法執(zhí)需要傳遞兩個參數(shù),分別提供數(shù)據(jù)的 key 與提供數(shù)據(jù) value.

7.1. 父組件

<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(注入),可以實現(xiàn)隔輩組件傳遞數(shù)據(jù)
import { provide, ref } from "vue";
let car = ref("保時捷911");

//祖先組件給后代組件提供數(shù)據(jù)
//兩個參數(shù):第一個參數(shù)就是提供的數(shù)據(jù)key
//第二個參數(shù):祖先組件提供數(shù)據(jù)
provide("CAR", car);
</script>

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

7.2. 子組件

<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>

7.3. 孫子組件可以通過 inject 方法獲取數(shù)據(jù),通過 key 獲取存儲的數(shù)值

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

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

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

// 使用 provide-inject 通信可以修改數(shù)據(jù)
const handler = () => {
  car.value = "自行車";
};
</script>

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

八、pinia

pinia的安裝和使用

http://www.lxweimin.com/p/f7f05ea09154

九、slot

插槽:默認(rèn)插槽、具名插槽、作用域插槽可以實現(xiàn)父子組件通信。

9.1 默認(rèn)插槽

在子組件內(nèi)部的模板中書寫 slot全局組件標(biāo)簽

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

<script lang='ts' setup>

</script>

<style scoped>

</style>

在父組件內(nèi)部提供結(jié)構(gòu), Children1 即為子組件,在父組件內(nèi)部使用的時候,在雙標(biāo)簽內(nèi)部書寫結(jié)構(gòu)傳遞給子組件。

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

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

</script>

<style scoped>

</style>

注意開發(fā)項目的時候默認(rèn)插槽一般只有一個。

9.2 具名插槽

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

下面是一個子組件內(nèi)部,模板中有兩個插槽:

<template>
  <div class="children1">
    <h2>我是子組件1</h2>
    <!-- 默認(rèn)插槽-->
    <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>

父組件內(nèi)部向指定的具名插槽傳遞結(jié)構(gòu), v-slot 可以替換為 #

<template>
  <div class="box">
    <h1>我是父組件:slot</h1>
    <div class="container">
      <Children1>
        <span>默認(rèn)插槽</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 作用域插槽

作用域插槽:子組件數(shù)據(jù)由父組件提供,但是子組件內(nèi)部決定不了自身結(jié)構(gòu)與外觀(樣式)

子組件 Children2 代碼如下:

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

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

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

父組件內(nèi)部代碼如下:

<template>
  <div class="box">
    <h1>我是父組件:slot</h1>
    <div class="container">
      <Children1>
        <span>默認(rèn)插槽</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";

//插槽:默認(rèn)插槽、具名插槽、作用域插槽
//作用域插槽:就是可以傳遞數(shù)據(jù)的插槽,子組件可以將數(shù)據(jù)回傳給父組件,父組件可以決定這些回傳的數(shù)據(jù)是以何種結(jié)構(gòu)或者外觀在子組件內(nèi)部去展示!!!

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

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

.container {
  display: flex;
  justify-content: space-between;
}
</style>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。