前言
不管是 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)父子組件通信
一、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ù)defineProps
是Vue3
提供方法,不需要引入直接使用- 數(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ù)。
defineEmits
是Vue3
提供的方法,不需要引入直接使用。defineEmits
方法執(zhí)行,傳遞一個數(shù)組,數(shù)組元素即為將來組件需要觸發(fā)的自定義事件類型,此方執(zhí)行會返回一個$emit
方法用于觸發(fā)自定義事件。
當(dāng)點擊按鈕的時候,事件回調(diào)內(nèi)部調(diào)用
$emit
方法去觸發(fā)自定義事件,第一個參數(shù)為觸發(fā)事件類型,第二個、第三個、第N個參數(shù)即為傳遞給父組件的數(shù)據(jù)。
- 在父組件內(nèi)部給子組件綁定一個自定義事件:
<Children2 @xxx="handler4" @click="handler5"></Children2>
- 在 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>
- 在父組件中接收子組件傳遞過來的參數(shù):
let handler4 = (params1, params2) => {
console.log(params1, params2)
}
let handler5 = (params1, params2) => {
console.log(params1, params2)
}
三、全局事件總線
全局事件總線可以實現(xiàn)任意組件通信,在
Vue2
中可以根據(jù)VM
與VC
關(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
分別是pageNo
與pageSize
,以及綁定兩個自定義事件update:pageNo
與update: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>