Vue组件通信的五种方式
文章目录
一. props/$emit(父子通信)
组件通信中最简单的通信方式,即props/emit,父子通信。
- props: 用于子组件获取父组件所传的值
①在子组件标签上采用 属性名=“属性值”
②在props中拿到父组件传过来的属性
使用场景:在父组件中已请求到数据,子组件中需要使用到时,通过props将父组件的数据传递给子组件。
<template>
<!-- 父组件 -->
<div class="parent">
<!-- =====================1. 在子组件标签上采用 属性名="属性值" -->
<child name="test"></child>
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
};
</script>
<template>
<!-- 子组件 -->
<div class="child">{{name}}</div>
</template>
<script>
export default {
props: {
// =====================2. 在props中拿到父组件传过来的属性
name: {
type: String,
default: "",
},
},
};
</script>
- $emit: 用于父组件监听子组件抛出的事件
①子组件通过this.$emit(”事件名“),往上抛出事件
②在子组件标签上采用 @子组件抛出的事件名=“父组件接收到子组件事件后所调用的方法”
使用场景:子组件向后端请求数据完成后,告诉父组件,数据请求成功,可以开始处理了。
<template>
<!-- 子组件 -->
<div class="child"></div>
</template>
<script>
export default {
methods: {
test() {
setTimeout(() => {
// ===================1. 1秒后数据请求成功,告诉父组件requestSuccess, 可以携带参数params
this.$emit("requestSuccess", params);
}, 1000);
},
},
};
</script>
<template>
<!-- 父组件 -->
<div class="parent">
<!-- =====================2. 在子组件标签上采用 @子组件抛出的事件名="父组件接收到子组件事件后所调用的方法" -->
<child @requestSuccess="parentReceived"></child>
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
methods: {
parentReceived(params) {
console.log("父组件接收成功", params);
},
},
};
</script>
二. vuex(组件之间通信)
- 什么是VueX?
官方解释:Vuex是一个专为Vue.js 应用程序开发的状态管理模式。
个人理解:组件存放公共变量的仓库。
上面讲到的props/emit可以理解为"通信",而VueX则更像是一个的仓库,组件们同时使用这个仓库,组件1在VueX中改变一个值后,其他组件中使用到这个变量的地方都会随之改变。总结来说就是:“公共”、“多个页面使用同一个状态”、“响应式”。
使用场景:登录成功后保存用户的登录状态、购物车中的物品等等
*注意!!VueX中的属性在浏览器刷新之后将全部初始化。
// ==========首先介绍一下VueX中的五大属性
import vuex from 'vuex'
const store = vuex.createStore({
// 1. state:单一状态树,简单理解就是存放VueX中的变量
// 通过this.$store.state.属性名获取
state: {
count: 0
},
// 2. getters:类似于vue中的comouted,返回state中的属性做处理之后的结果
// 通过this.$store.getters.属性名获取
getters: {
countAdd(state) {
return state.count + 1
}
},
// 3. mutations:组件想改变VueX中的state,唯一的方法就是提交mutation
// 通过this.$store.commit(方法名, payload) 可携带参数payload
mutations: {
// 第一个参数通常是state,第二个参数可以是this.$store.commit所携带的参数
increment(state, payload) {
state.count++
}
},
// 4. actions:通过提交mutation来改变state,其中可以加入异步操作,可以理解为升级版的异步mutations
// 通过this.$store.dispatch(方法名, payload) 可携带参数payload
actions: {
// 与mutations不同,第一个参数通常是vuex对象,第二个参数可以是this.$store.dispatch所携带的参数
// 可以理解为context 《=》 this.$store
increment(context, payload) {
context.commit('increment')
},
// 通常采用对象解构赋值写为:
increment2({ commit, state, getters }, payload) {
commit('increment')
}
},
// 5. modules:通常在模块化开发时使用,小项目不推荐使用
// 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
modules: {
modulesA: {
state: {},
getters: {},
mutations: {},
actions: {},
},
modulesB: {
state: {},
getters: {},
mutations: {},
actions: {},
},
}
})
- 组件用例
<template>
<!-- 子组件 -->
<div class="child">
<!-- 子组件如果改变VueX中的userName,此处也会立即变化 -->
{{userName}}
</div>
</template>
<script>
export default {
computed: {
userName() {
// 通过this.$store.state.属性名获取
return this.$store.state.userName;
},
},
methods: {
test() {
// 通过this.$store.commit提交mutation来修改
this.$store.commit("setUserName", "测试用户")
}
}
};
</script>
<template>
<!-- 父组件 -->
<div class="parent">
<!-- 子组件如果改变VueX中的userName,此处也会立即变化 -->
{{userName}}
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
computed: {
userName() {
// 通过this.$store.state.属性名获取
return this.$store.state.userName;
},
},
};
</script>
三. 事件总线EventBus(组件之间通信)
EventBus与VueX类似,也可以理解为状态管理仓库,但EventBus并没有state、getters等概念,因此跟VueX相比更像是一个”杂乱“的状态管理仓库。EventBus更像是一个事件中心,组件A向事件中心发送消息,其他组件监听这个消息即可。
使用方法:
- 初始化EventBus
①局部事件总线:新创建一个 .js 文件,比如event-bus.js
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
实质上EventBus是一个不具备 DOM 的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。
②全局事件总线:可以直接在项目中的 main.js 初始化EventBus
// main.js
Vue.prototype.$EventBus = new Vue()
- 使用用例
// 向EventBus发送消息
EventBus.$emit(事件名, 回调函数)
// 监听EventBus接收到的消息
EventBus.$on(事件名, 回调函数)
// 移除监听
EventBus.$off(事件名, 回调函数)
<!-- A.vue -->
<template>
<button @click="sendMsg()">-</button>
</template>
<script>
// 导入局部EventBus
import { EventBus } from "../event-bus.js";
export default {
methods: {
sendMsg() {
// ==============1. 通过EventBus.$emit(事件名, 参数),向事件总线中抛出事件
EventBus.$emit("aMsg", '来自A页面的消息');
}
}
};
</script>
<!-- B.vue -->
<template>
<p>{{msg}}</p>
</template>
<script>
import { EventBus } from "../event-bus.js";
export default {
data(){
return {
msg: ''
}
},
mounted() {
// ==============2. 在mounted中创建EventBus的监听器,监听对应事件,并添加回调方法
EventBus.$on("aMsg", (msg) => {
// A发送来的消息
this.msg = msg;
});
},
beforeDestroy() {
// ==============3. 关闭页面时,在beforeDestroy中使用EventBus.$off关闭监听器
EventBus.$off("aMsg", (msg) => {
// A发送来的消息
this.msg = msg;
});
}
};
</script>
*注意:前面提到过,如果使用不善,EventBus会是一种灾难,到底是什么样的”灾难“了?大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。还要就是如果业务有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus在项目中的关系。通常会用到,在vue页面销毁时,同时移除EventBus事件监听。
四. provide提供/inject注入(祖先与后代之间通信)
provide与inject通常需要组合使用,以允许一个祖先组件(provide)向其所有子孙后代(inject)注入一个依赖,不论组件层级有多深,后代组件总能拿到祖先所提供的属性。
①在祖先组件中使用provide提供相关属性。
②在后代元素中使用inject拿到祖先组件提供的属性,并注入到当前组件中,即可在当前组件中使用。
*注意:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
使用用例:
<template>
<!-- 祖先组件 -->
<div class="ancestors">
<parent></parent>
</div>
</template>
<script>
import parent from "./parent.vue";
export default {
components: { parent },
data() {
return {
userName: "test",
};
},
// ==================1. 祖先组件使用provide提供对应属性,相当于export了
provide() {
return {
userName: this.userName,
};
},
};
</script>
<template>
<!-- 父组件 -->
<div class="parent">
<child></child>
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
// =====================2. 后代元素中使用inject注入该属性,相当于import
inject: ["userName"],
};
</script>
<template>
<!-- 子组件 -->
<div class="child">
{{userName}}
</div>
</template>
<script>
export default {
// ====================2. 后代元素中使用inject注入该属性,相当于import
inject: ["userName"],
};
</script>
五. a t t r s / attrs/ attrs/listeners(父子通信)
使用场景:
①组件传值时使用: 爷爷在父亲组件传递值,父亲组件会通过 a t t r s 获 取 到 不 在 父 亲 p r o p s 里 面 的 所 有 属 性 , 父 亲 组 件 通 过 在 孙 子 组 件 上 绑 定 attrs获取到不在父亲props里面的所有属性,父亲组件通过在孙子组件上绑定 attrs获取到不在父亲props里面的所有属性,父亲组件通过在孙子组件上绑定attrs 和 $listeners 使孙组件获取爷爷传递的值并且可以调用在爷爷那里定义的方法;
②对一些UI库进行二次封装时使用:比如element-ui,里面的组件不能满足自己的使用场景的时候,会二次封装,但是又想保留他自己的属性和方法,那么这个时候时候 a t t r s 和 attrs和 attrs和listners是个完美的解决方案。
- $attrs
一个祖先元素需要向后代传递多个元素时,我们通常采用这种写法:
<template>
<!-- 祖先组件 -->
<div class="ancestors">
<!-- =====================1. 使用属性名="属性值"的方式逐个传递值 -->
<parent name="test" age="20" sex="1"></parent>
</div>
</template>
<script>
import parent from "./parent.vue";
export default {
components: { parent },
};
</script>
而后代元素获取这些父元素传递过来的值时,一般情况下都需要使用props属性逐一获取,这种方式在传递属性较多且属性值复杂时,往往容易出错:
<template>
<!-- 父组件 -->
<div class="parent">
{{name}}
{{age}}
{{sex}}
<child></child>
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
// 2. =================使用props逐一获取对应属性
props: ["name", "age", "sex"],
};
</script>
a t t r s 实 际 上 就 是 p r o p s 的 升 级 版 , 后 代 元 素 取 值 时 , 可 以 不 通 过 p r o p s 直 接 使 用 attrs实际上就是props的升级版,后代元素取值时,可以不通过props直接使用 attrs实际上就是props的升级版,后代元素取值时,可以不通过props直接使用attrs取到所有传递下来的属性:
<template>
<!-- 父组件 -->
<div class="parent">
{{name}}
<!-- =====================3. 直接使用$attrs取值 -->
{{$attrs.age}}
{{$attrs.sex}}
<!-- =================4. 可以直接使用v-bind="$attrs"将参数继续向下传递 -->
<child v-bind="$attrs" head="hhhh"></child>
<!-- 相当于 <child name="test" age="20" sex="1" head="hhhh"></child> -->
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
// 2. 使用props逐一获取对应属性,******注意:props优先级高于$attrs,一旦在props中取了该属性,$attrs便不再有该属性。
props: ["name"],
};
</script>
*总结:①$attrs可以一次拿到所有属性(class、style、props已经声明的除外)。
②个人理解: a t t r s 只 是 一 种 简 写 , 原 理 与 p r o p s 类 似 , 不 能 跨 层 级 传 递 , 需 要 一 层 一 层 使 用 v − b i n d = " attrs只是一种简写,原理与props类似,不能跨层级传递,需要一层一层使用v-bind=" attrs只是一种简写,原理与props类似,不能跨层级传递,需要一层一层使用v−bind="attrs"依次传递。
③如果除 a t t r s 外 还 有 其 他 额 外 参 数 需 要 传 递 , 额 外 参 数 与 attrs外还有其他额外参数需要传递,额外参数与 attrs外还有其他额外参数需要传递,额外参数与attrs中的参数重复时,额外参数优先级更高。
- $listeners
与 a t t r s 类 似 , attrs类似, attrs类似,listeners包含了所有父组件中的事件监听器(使用.native修饰的事件除外)。
父组件监听子组件事件时,通常采用这种写法:
<template>
<!-- 祖先组件 -->
<div class="ancestors">
<!-- =====================1. 使用@事件名="方法名"的方式逐个监听 -->
<parent @click.native="clickHandle" @test="testHandle"></parent>
</div>
</template>
<script>
import parent from "./parent.vue";
export default {
components: { parent },
};
</script>
而在子组件中,我们只需要$emit出对应事件名既可以触发父组件对应方法:
<template>
<!-- 父组件 -->
<div class="parent">
<child></child>
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
created() {
// ===================3. 这里使用this.$listeners就可以直接拿到祖先组件监听的所有事件
console.log(this.$listeners)
},
methods: {
test() {
// ==================2. 使用$emit抛出事件,触发父组件方法。
this.$emit('test')
}
}
};
</script>
同 a t t r s 一 样 , 可 以 通 过 v − o n = " attrs一样,可以通过 v-on=" attrs一样,可以通过v−on="listeners" 将事件监听器继续向下传递,后代元素抛出对应事件后,同样可以触发祖先组件中的方法。
如果想要添加其他事件监听器,可继续绑定事件。但要注意的是,继续绑定的事件和 $listeners 中的事件有重复时,不会被覆盖。当 grandson.vue 触发 customEvent 时,child.vue 和 parent.vue 的事件都会被触发,触发顺序类似于冒泡,先到 child.vue 再到 parent.vue。