vue2.0通信方式大全
前言
vue是数据驱动视图
, 所以对于vue来说组件间的数据通信非常重要。vue组件通信也是面试中常问到的面试题。熟练的掌握vue组件通信对于我们来说至关重要。那么组件之间如何进行数据通信的呢?本篇文章较长,将比较全面的讲述各种通信方式,应该是最全的关于vue通信的文章了,若有遗漏,欢迎指出。下面将围绕父子通信组件
和非父子组件通信
展开,将配上代码讲解、结果示例图。
如果对你有用,敬请收藏,也是对原创作者的鼓励!
首先,我们要明白,vue组件通信
主要分为:
- 父子组件之间通信(又具体分为父组件向子组件传值、子组件向父组件传值):
props
、$emit
、.sync
、v-model
、ref
、$parent
和$children
- 父组件跟孙子组件的通信:
$attr
和$listeners
、provide
和inject
- 非父子组件之间通信:
eventbus
、vuex
一、父子组件之间通信
1. props(父-》子)
任何类型的值都可以传给一个 prop,父元素可以通过prop传值给子元素。
下面通过一个例子说明父组件如何向子组件传递数据:在子组件child.vue
中获取父组件parent.vue
中的数据 title: 周末安排
以及 todoList: ['吃饭', '睡觉', '打豆豆']
// 父组件 parent.vue
<template>
<child :title="title" :list="todoList"></child>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
title: "周末安排",
todoList: ['吃饭', '睡觉', '打豆豆']
}
},
components:{
child
},
mounted() {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
</style>
// 子组件 child.vue
<template>
<div class="todos">
<p class="title">{{title}}</p>
<ul class="list">
<li v-for="item in list" :key="item">{{item}}</li>
</ul>
</div>
</template>
<script>
export default {
props: ['title','list'],
data() {
return {
}
},
components:{
},
mounted() {
},
methods: {
}
}
</script>
<style lang="scss">
.todos{
margin: 20px;
.title{
font-size: 20px;
margin-bottom: 10px;
}
.list{
list-style: square inside;
font-size: 18px;
}
li{
padding: 5px 5px;
}
}
</style>
props还可以设置默认值
,如下所示:
//child.vue
props: {
title: {
type: String,
default: '默认-工作日'
},
list: {
type: Array,
default: function () {
// 对象或数组Array默认值必须从一个工厂函数获取
return ['上班搬砖','喝快乐肥宅水','继续码']
}
}
},
这时候,如果你在父组件parent.vue
中没传 title 跟 list 的话,将展示如下:
props 的 type 值为:
String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
注意:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告
简而言之:
prop是单向下行绑定的.
2. $emit(子-》父)
$emit
可以实现子组件向父组件传值。在子组件中通过$emit
注册事件,将数据作为参数传入,在父组件中通过$event
接收。
在上个例子的基础上, 我们来实现点击页面渲染出来的item, 父组件显示点击的数据
// 父组件 parent.vue
<template>
<div class="todos-wrap">
<child @clickTodo="clickTodo"></child>
<p v-if="clickValue">点击了:{{clickValue}}</p>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
title: "周末安排",
todoList: ['吃饭', '睡觉', '打豆豆'],
clickValue: '',
}
},
components:{
child
},
mounted() {
},
methods: {
clickTodo($event){
this.clickValue = $event
}
}
}
</script>
<style lang="scss" scoped>
.todos-wrap{
margin: 20px;
font-size: 18px;
}
</style>
// 子组件 child.vue
<template>
<div class="todos">
<p class="title">{{title}}</p>
<ul class="list">
<li v-for="item in list" :key="item" @click="$emit('clickTodo', item)">{{item}}</li>
</ul>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '默认-工作日'
},
list: {
type: Array,
default: function () {
// 对象或数组Array默认值必须从一个工厂函数获取
return ['上班搬砖','喝快乐肥宅水','继续码']
}
}
},
// props: ['title','list'],
data() {
return {
}
},
components:{
},
mounted() {
},
methods: {
}
}
</script>
<style lang="scss">
.todos{
.title{
font-size: 20px;
}
.list{
list-style: square inside;
margin: 10px 0;
}
li{
padding: 5px 5px;
cursor: pointer;
}
}
</style>
3. .sync(父子双向通信)
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。这时候 .sync
就上场了。.sync
修饰符是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on
监听器
<child :isShow.sync="showValue"></child> // 是语法糖,最后会被解析成
<child :isShow="showValue" @update:isShow="val => showValue = val"></child>
然后子组件就这样向父组件传值即可,
this.$emit('update:isShow', newValue)
如果还看不懂,老规矩,咱们来写代码试试。假如我们要实现点击开关实现详情文字显示/隐藏
,那可以这么做:
// 父组件 parent.vue
<template>
<div class="todos-wrap">
<p>这是一行标题</p>
<child :isShow.sync="showValue"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
showValue: true
}
},
components:{
child
},
mounted() {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.todos-wrap{
margin: 20px;
font-size: 18px;
}
</style>
// 子组件 child.vue
<template>
<div class="todos">
<p v-if="isShow">这是一段详情。这是一段详情。这是一段详情。</p>
<el-button @click="toggleShow">显示/隐藏详情</el-button>
</div>
</template>
<script>
export default {
props: ['isShow'],
data() {
return {
}
},
components:{
},
mounted() {
},
methods: {
toggleShow(){
this.$emit('update:isShow', !this.isShow)
}
}
}
</script>
<style lang="scss">
.todos{
.title{
font-size: 20px;
}
.list{
list-style: square inside;
margin: 10px 0;
}
li{
padding: 5px 5px;
cursor: pointer;
}
}
</style>
实现效果
vue 自定义组件使用v-model,可以实现同样的功能。让我们用v-model重写这个功能,来看看吧。
4. v-model(父子双向通信)
v-model
可以在自定义组件上使用,可以实现双向通信。v-model
其实也是个语法糖。比如说,
<input v-model="something">// 是语法糖,最后会被解析成
<input :value="something" @input="something = $event.target.value">
那了解了理论知识,就让我们动手写下刚刚用.sync
实现的功能吧。
// 父组件 parent.vue
<template>
<div class="todos-wrap">
<p>这是一行标题</p>
<child v-model="showValue"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
showValue: true
}
},
components:{
child
},
mounted() {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.todos-wrap{
margin: 20px;
font-size: 18px;
}
</style>
// 子组件 child.vue
<template>
<div class="todos">
<p v-if="value">这是一段详情。这是一段详情。这是一段详情。</p>
<el-button @click="toggleShow">显示/隐藏详情</el-button>
</div>
</template>
<script>
export default {
props: ['value'], //接收一个 value prop,注意,这里用的是value
data() {
return {
}
},
components:{
},
mounted() {
},
methods: {
toggleShow(){
this.$emit('input', !this.value); //触发 input 事件,并传入新值
}
}
}
</script>
<style lang="scss">
.todos{
.title{
font-size: 20px;
}
.list{
list-style: square inside;
margin: 10px 0;
}
li{
padding: 5px 5px;
cursor: pointer;
}
}
</style>
5. ref(父调用子的方法或访问子的数据)
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素
;如果在子组件上引用,就指向组件实例
,我们可以通过实例直接调用组件的方法或访问数据, 下面举个例子,如何通过父组件获取子组件的值 以及 控制子组件的值+1
// 父组件 parent.vue
<template>
<div class="todos-wrap">
<child ref="child"></child>
<el-button @click="changeChildValue">plus</el-button>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
}
},
components:{
child
},
mounted() {
},
methods: {
changeChildValue(){
const child = this.$refs.child;
console.log(child.number); // 原始数据
child.changeValue(); // 数据发生了改变
}
}
}
</script>
<style lang="scss" scoped>
.todos-wrap{
margin: 20px;
font-size: 18px;
}
</style>
// 子组件 child.vue
<template>
<div class="todos">
<p>{{number}}</p>
</div>
</template>
<script>
export default {
data() {
return {
number: 1
}
},
components:{
},
mounted() {
},
methods: {
changeValue(){
this.number ++
}
}
}
</script>
<style lang="scss">
.todos{
.title{
font-size: 20px;
}
.list{
list-style: square inside;
margin: 10px 0;
}
li{
padding: 5px 5px;
cursor: pointer;
}
}
</style>
6.$children
/ $parent
我们对上面的例子继续改造。如果用$children
/ $parent
实现通信。直接上代码:
// 父组件 parent.vue
<template>
<div class="todos-wrap">
<child></child>
<el-button @click="changeChildValue">plus</el-button>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
msg: '父组件的值'
}
},
components:{
child
},
mounted() {
},
methods: {
changeChildValue(){
console.log(this.$children[0].number); // 注意:this.$children是个数组
this.$children[0].changeValue(); // 数据发生了改变
}
}
}
</script>
<style lang="scss" scoped>
.todos-wrap{
margin: 20px;
font-size: 18px;
}
</style>
// 子组件 child.vue
<template>
<div class="todos">
<p>{{parentVal}}</p>
<p>{{number}}</p>
</div>
</template>
<script>
export default {
data() {
return {
number: 1
}
},
components:{
},
mounted() {
},
computed:{
parentVal(){
return this.$parent.msg;
}
},
methods: {
changeValue(){
this.number ++
}
}
}
</script>
<style lang="scss">
.todos{
.title{
font-size: 20px;
}
.list{
list-style: square inside;
margin: 10px 0;
}
li{
padding: 5px 5px;
cursor: pointer;
}
}
</style>
得到的效果如下:
注意:
this.$parent
和this.$children
返回的值不一样,this.$children
的值是数组,而this.$parent
是个对象- 在#app上的
this.$parent
得到的是new Vue()的实例,在这实例上再this.$parent
得到的是undefined,而在最底层的子组件的this.$children
是个空数组
二、父-子-孙组件之间通信
1. $attr
和 $listeners
当我们写高级别的组件的时候,如果有N个props
以及N个$emit
触发的事件,可以用$attr
和$listeners
轻松解决,否则的话每一个从父组件传到子组件的props,我们都得在子组件的 props 中显式的声明才能使用。
这样一来,我们的子组件每次都需要申明一大堆 props. 遇到多级组件嵌套的情况代码会显得非常的冗余,有了$attr
和$listeners
不用将 props 一层一层往下传递。
$attrs、$listeners
都是可以跨域父子组件,可以父 – 子 – 孙组件传递。下面举个例子,用$attrs、$listeners
实现父组件parent.vue
跟孙子组件grandson.vue
的通信。
// parent.vue 父组件
<template>
<div class="parent">
<Child
class="origin"
style="color:red"
:message="message"
:number="this.number"
@upNumber="upNumber"
@input="(event) => { message = event }"
/>
</div>
</template>
<script>
import Child from "./child";
export default {
components: {
Child
},
data() {
return {
message: "parent",
number: 0
};
},
methods: {
upNumber (event) {
this.number = event;
}
},
};
</script>
<style lang="scss">
.parent{
padding: 20px;
}
</style>
// child.vue 子组件
<template>
<Grandson
v-bind="$attrs"
v-on="$listeners"
/>
</template>
<script>
import Grandson from "./grandson";
export default {
inheritAttrs:false,
components: {
Grandson
},
};
</script>
// grandson.vue 孙子组件
<template>
<div class="children" style="font-size:18px">
{{$attrs.message}}
<p @click="$listeners.upNumber($attrs.number + 1)">点击数字实现递增{{$attrs.number}}</p>
</div>
</template>
<script>
export default {
inheritAttrs:false,
mounted() {
console.log(this.$attrs); // 不包含class 和 style
console.log(this.$listeners);
setTimeout(() => {
// 用$emit跟$listeners都行
// this.$emit("input", "children");
// this.$emit('upNumber', this.$attrs.number + 1)
this.$listeners.input("children")
this.$listeners.upNumber(this.$attrs.number + 1)
}, 1500);
}
};
</script>
实现效果如下:
注意: $attr
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class
和 style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和 style
除外),并且可以通过 v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
inheritAttrs
inheritAttrs
默认是true, 如果没设置为false的话,父作用域的不被认作 props 的特性绑定将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。如果我们设置 inheritAttrs:false
,这些默认行为将会被去掉。是不是觉得特别晦涩难懂,别慌~ 看下面的两张对比图,相信你就懂了!
2. provide
和 inject
provide/ inject
是vue2.2.0新增的api, 简单点来说就是父组件中通过provide
来提供变量, 然后再子组件中通过inject
来注入变量。
provide
和inject
主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。provide
和inject
允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
// parent.vue 父组件
<template>
<div class="parent">
<Child />
</div>
</template>
<script>
import Child from "./child";
export default {
components: {
Child
},
provide: {
message: 'parent'
},
};
</script>
<style lang="scss">
.parent{
padding: 20px;
}
</style>
// child.vue 子组件
<template>
<Grandson
v-bind="$attrs"
v-on="$listeners"
/>
</template>
<script>
import Grandson from "./grandson";
export default {
components: {
Grandson
},
mounted() {
}
};
</script>
// grandson.vue 孙子组件
<template>
<div class="children" style="font-size:18px">
{{message}}
</div>
</template>
<script>
export default {
inject: ['message'],
};
</script>
三、非父子组件之间通信:
1.eventbus
eventBus
又称为事件总线,在vue中可以使用它来作为沟通桥梁, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。
针对中大型项目, 首选Vuex, 但是如果是小型项目使用Vue的eventBus, 是一个不错的选择。
全局的eventBus简单理解为在一个文件创建一个新的vue实例然后暴露出去, 使用的时候import这个模块进来即可。
我们在来实现comp2.vue
向comp1.vue
传递数据。做个简单的累加器。
// parent.vue
<template>
<div>
<comp1></comp1>
<comp2></comp2>
</div>
</template>
<script>
import comp1 from './comp1.vue'
import comp2 from './comp2.vue'
export default {
components: { comp1, comp2 }
}
</script>
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// parent.vue
<template>
<div>
<comp1></comp1>
<comp2></comp2>
</div>
</template>
<script>
import comp1 from './comp1.vue'
import comp2 from './comp2.vue'
export default {
components: { comp1, comp2 }
}
</script>
// comp1.vue 中接收事件
<template>
<div>计算和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('add', param => {
this.count = param.num;
})
}
}
</script>
// comp2.vue 中发送事件
<template>
<div>
<el-button @click="additionHandle">+累加</el-button>
</div>
</template>
<script>
import {EventBus} from './event-bus.js'
console.log(EventBus)
export default {
data(){
return{
num:1
}
},
methods:{
additionHandle(){
EventBus.$emit('add', {
num: this.num++
})
}
}
}
</script>
缺点: 当项目较大, eventBus难以维护
2.vuex
vuex这里就不赘述了,建议直接移步官方文档
总结
下面,再来一张思维导图来阐述通信的主要方式。
写在最后
这篇文章断断续续写了一周,主要是这段时间看了些vue3.0
,觉得很多东西都是共通的。便想对自己最熟悉的vue2.0
做个总结。毕竟,vue3.0
的composition
写法对代码改动还是很大的。重构代码需要花时间,目前还是以vue2.0为主吧。