组件间通信
- 不要在子组件中直接修改父组件的状态数据;
- 数据在哪, 更新数据的行为 (函数) 就应该定义在哪;
根据组件之间关系讨论组件通信
-
父子组件:
- $dispatch 和 $broadcast ,vue1.x 提供,前者用于向上级派发事件,只要是它的父级 (一级或多级以上),都可以在组件内通过 $on (或 events,2.x 已废弃) 监听到,后者相反,是由上级向下级广播事件;
- props / $emit:父组件向子组件传递数据是通过 props 传递的,子组件传递数据给父组件是通过 $emit 触发事件来做到的;
- $on / $emit:通过 $on 监听当前实例的事件;
- $parent / $children:
- ref:获取实例;
-
兄弟组件:
- $parent
- eventbus 事件总线;
- vuex 状态管理;
-
跨层级关系:
- $attrs / $listeners:A->B->C,vue2.4 提供了 $attrs / $listeners 来解决这个问题;
- provide / inject:父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量;
- $root
- eventbus 事件总线;
-
vuex 状态管理;
图解
通信方案
props / $emit
-
子组件接收的属性值类型
- 原生属性类型:String、Number、Boolean、Array、Object、Date、Function、Symbol
- 自定义属性类型:
<script> function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } export default { props: { author: Person, }, }; </script>
-
vue 的单向数据流
- 父级 prop 的更新会向下流动到子组件中,但是反过来则不行;
- 这样会防止从子组件意外变更父级组件的状态,从而导致应用的数据流向难以理解;
- 每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值;
-
示例代码
在父组件的子组件标签中绑定自定义属性
<template> <!-- 子组件 user-detail --> <user-detail :myName="name" :childMsg="childMsg" @handleChange="changeName"/> </template> <script> export default { components: { UserDetail, }, data() { return { name: '张三', childMsg: [1, 2, 3], }; }, methods: { changeName(name) { console.log(name); }, }, }; </script>
在子组件中使用 props 接收即可,可以传多个属性
<template> <!-- 子组件 --> <button @click="changeParentName">改变父组件的name</button> </template> <script> export default { props: { // 多个可能的类型 myName: [String, Number], childMsg: { type: Array, requires: true, // 必填 default: () => [1], // default 指定默认值 validator: value => { // 这个值必须匹配下列字符串中的一个 return ['success', 'warning', 'danger'].includes(value); }, }, }, methods: { // 子组件的事件 changeParentName: function () { // 触发父组件中 handleChange 事件并传参 Jack // 触发当前组件中 handleChange 事件并传参 Jack this.$emit('handleChange', 'Jack'); }, }, }; </script>
$emit / $on
-
$emit 和
$on主要解决的问题是事件的定义和消费:$on监听当前实例上自定义事件,EventBus 的实现方式(vue3 已废弃);- $emit 中消费这个事件,负责发送数据;
-
示例代码
在父组件中定义并绑定 handleChange 事件
<template> <!-- 父组件 自定义事件 handleChange --> <child @handleChange="changeName"></child> </template> <script> import child from './child.vue'; export default { components: { child, }, methods: { changeName(name) { // 输出 Jack console.log(name); }, }, }; </script>
在子组件中消费 handleChange 这个事件,并把数据发送给父组件
<template> <!-- 子组件 --> <button @click="changeParentName">改变父组件的name</button> </template> <script> export default { methods: { // 子组件的事件 changeParentName: function () { // 触发父组件中 handleChange 事件并传参 Jack // 触发当前组件中 handleChange 事件并传参 Jack this.$emit('handleChange', 'Jack'); }, }, mounted() { this.$on("handleChange", (val) => { // 输出 Jack console.log(val); }); }, }; </script>
$parent / $children
-
$parent:获取父亲组件实例;
-
$children:获取孩子组件实例,返回一个 [],不是响应式的(vue3 已废弃); -
示例代码
- 父组件
<template> <div style="background-color: #999"> <button @click="getChildren">获取孩子</button> <son></son> <p></p> <daughter></daughter> </div> </template> <script> import Son from './son.vue'; import Daughter from './daughter.vue'; export default { name: 'Father', components: { Son, Daughter, }, data() { return { fatherMoney: 20000, }; }, methods: { getChildren() { console.log(this.$children); }, }, }; </script>
- 子组件1
<template> <div style="background-color: #eee; height:100px;"> 儿子 <button @click="getParent">获取父亲</button> </div> </template> <script> export default { name: 'son', props: ['fatherMoney'], data() { return { sonMoney: 20000, }; }, methods: { getParent() { console.log(this.$parent); }, }, }; </script>
- 子组件2
<template> <div style="background-color: #eee ;height:100px;"> 女儿 <button @click="getParent">获取父亲</button> </div> </template> <script> export default { name: 'Daughter', props: ['fatherMoney'], data() { return { daughterMoney: 18000, }; }, methods: { getParent() { console.log(this.$parent); }, }, }; </script>
- 父组件
-
效果展示
- 打印 $children(两个子组件实例)
- 打印 $parent(父组件实例)
- 打印 $children(两个子组件实例)
ref
-
ref 的第一种用法:ref 加在普通的元素上,用 this.$refs.ref值 获取到的是 dom 元素,在使用的时候确保 dom 已经渲染完成,比如在生命周期 mounted 或者在 $nextTick 中调用;
<template> <div> <input ref="input" name="name" value="张三" /> </div> </template> <script> export default { data() { return {}; }, mounted() { // 输出 dom 元素 console.log(this.$refs.input); }, }; </script>
-
ref 的第二种用法:ref 加在子组件上 , 用 this.$refs.ref值 获取到的就是组件实例;
<template> <div> <son ref="son"></son> </div> </template> <script> import Son from "./son.vue"; export default { name: "Father", components: { Son, }, data() { return {}; }, mounted() { // 输出子组件实例 console.log(this.$refs.son); }, }; </script>
-
ref 的第三种用法:ref 获取实例数组或者 dom 节点数组;
<template> <div> <button @click="getRefs">获取 ref 数组</button> <son :ref="formDom"></son> <son :ref="formDom"></son> <son :ref="formDom"></son> <p :ref="formDom">1</p> <p :ref="formDom">2</p> <p :ref="formDom">3</p> </div> </template> <script> export default { data() { return { dom: [], }; }, methods: { formDom(el) { this.dom.push(el); }, getRefs() { // 输出 3个实例、3个dom元素 的数组 console.log(this.dom); }, }, }; </script>
$attrs / $listeners
$attrs:(父组件 -> 孙子通信,通过 $attrs 属性透传)
- 继承所有的父组件属性(除了 props 接收的属性还有 class 类名 和 style 样式之外的属性);
- inheritAttrs:默认值 true,就是继承所有的父组件属性(除了 props 绑定外)作为普通的 HTML 特性应用在子组件的根元素上,如果不希望组件的根元素继承特性就设置 inheritAttrs: false ,但是 class、style 还是会继承合并;
示例代码:
<!-- 父组件 -->
<template>
<div style="background: #eee">
<p>父组件</p>
<son
class="theme-dark" style="background: yellow"
:fatherFirstName="fatherFirstName" :fatherAge="fatherAge" :fatherMoney="fatherMoney"
></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
components: {
son,
},
data() {
return {
fatherFirstName: "张三",
fatherAge: "40",
fatherMoney: "300000",
};
},
};
</script>
<!-- 子组件 -->
<template>
<!-- 由于当前组件只获取了 fatherMoney,则 fatherFirstName、fatherAge 会被绑定在该 html 上 -->
<div style="background: red; padding: 30px">
<p @click="getAttrs">子组件</p>
<!-- 通过 v-bind="$attrs" 把外部传入的非 prop 属性设置给希望的标签上-->
<grand-son v-bind="$attrs"></grand-son>
</div>
</template>
<script>
import GrandSon from "./grandSon.vue";
export default {
inheritAttrs: true,
props: {
fatherMoney: String,
},
components: {
GrandSon,
},
methods: {
getAttrs() {
// {fatherFirstName: '张三', fatherAge: '40'}
console.log(this.$attrs);
},
},
};
</script>
<!-- 孙子组件 -->
<template>
<p style="background: green">孙子组件</p>
</template>
<script>
export default {
props: {
fatherFirstName: String,
fatherAge: String,
fatherMoney: String,
},
};
</script>
效果展示
$listeners:(孙子组件 -> 父组件通信,通过$listeners事件透传)
- 它是一个对象,能接收所有的方法绑定,里面包含了作用在这个组件上的所有监听器,配合 v-on=“$listeners” 将所有的事件监听器指向这个组件的某个特定的子元素;
- Vue3 已经不支持 $listeners 了,Vue2 的 $listeners 在 Vue3 中是合并到了 $attrs 里;
示例代码:
<!-- 父组件 -->
<template>
<div style="background: #eee">
<p>父组件</p>
<son
class="theme-dark"
style="background: yellow"
:fatherFirstName="fatherFirstName"
:fatherAge="fatherAge"
:fatherMoney="fatherMoney"
@fun1="fun1"
></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
components: {
son,
},
data() {
return {
fatherFirstName: "张三",
fatherAge: "40",
fatherMoney: "300000",
};
},
methods: {
fun1(val) {
// 我是您孙子
console.log(val);
},
},
};
</script>
<!-- 子组件 -->
<template>
<div style="background: red; padding: 30px">
<p @click="getListeners">子组件</p>
<grand-son v-bind="$attrs" v-on="$listeners"></grand-son>
</div>
</template>
<script>
import GrandSon from "./grandSon.vue";
export default {
inheritAttrs: true,
props: {
fatherMoney: String,
},
components: {
GrandSon,
},
methods: {
getListeners() {
// {fun1: ƒ}
console.log(this.$listeners);
},
},
};
</script>
<!-- 孙子组件 -->
<template>
<p @click="toParent" style="background: green">孙子组件</p>
</template>
<script>
export default {
props: {
fatherFirstName: String,
fatherAge: String,
fatherMoney: String,
},
methods: {
toParent() {
// 触发父组件的方法
this.$emit("fun1", "我是您孙子");
},
},
};
</script>
EventBus 事件总线
-
什么是事件总线
- EventBus 作为沟通桥梁,就像是所有组件共用相同的事件中心,可以向该中心「注册发送事件」或「接收事件」,所有组件都可以上下平行地通知其他组件;
- 但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”,因此才需要更完善的 Vuex 作为状态管理中心,将通知的概念上升到共享状态层次;
-
示例代码
- EventBus.vue:首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它
import Vue from "vue"; export const EventBus = new Vue();
- childOne.vue:
<template> <div>{{ count01 }}</div> </template> <script> import { EventBus } from "./event-bus.js"; export default { data() { return { count01: 0, }; }, mounted() { EventBus.$on("aMsg01", (count) => { // childTwo发送来的消息 this.count01 = count; }); }, }; </script>
- childTwo.vue:
<template> <button @click="sendMsg()">+</button> </template> <script> import { EventBus } from "./event-bus.js"; export default { data() { return { count: 0, }; }, methods: { sendMsg() { this.count++; EventBus.$emit("aMsg01", this.count); }, }, }; </script>
- EventBus.vue:首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它
provide / inject
-
provide / inject:在父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量;
- 不论子组件有多深,只要调用了 inject 那么就可以注入 provide 中的数据;
- provide 和 inject 绑定并不是可响应的,然而,如果传入了一个响应式的对象,那么其对象的属性还是响应的;
-
示例代码
HTMLHTMLHTML<!-- 父组件 --> <template> <div style="background: #eee"> <p>父组件</p> <input v-model="fatherMoney" /> <input v-model="motherMoney" /> <son></son> </div> </template> <script> import son from "./son.vue"; export default { components: { son, }, provide() { return { FATHER: this, // 传递当前实例对象,因为是响应式的对象,索引 provide 和 inject 也是响应式的 MOTHER_MONEY: this.motherMoney, }; }, data() { return { fatherMoney: "300000", motherMoney: "400000", }; }, }; </script>
<!-- 子组件 --> <template> <div style="background: red; padding: 30px"> <p>子组件</p> <grand-son></grand-son> </div> </template> <script> import GrandSon from "./grandSon.vue"; export default { components: { GrandSon, }, }; </script>
<!-- 孙子组件 --> <template> <p @click="upprovide" style="background: green"> 孙子组件{{ `响应式:${FATHER.fatherMoney} ---- 非响应式:${MOTHER_MONEY}` }} </p> </template> <script> export default { inject: ["FATHER", "MOTHER_MONEY"], methods: { upprovide() { // 这里修改之后 父组件 也会响应数据的变化 this.FATHER.fatherMoney = "200000"; // 这里修改之后 父组件 不会变化,并报错 this.MOTHER_MONEY = "200000"; }, }, }; </script>
$root
-
$root:获取根组件实例;
-
示例代码
<template> <p @click="getRootRef" style="background: green">获取根组件实例</p> </template> <script> export default { methods: { getRootRef() { console.log(this.$root); }, }, }; </script>
-
效果展示
vue2.x✍️ vue2.x VS vue3.x 的响应式
上一篇