vue2.0通信方式大全

前言

vue是数据驱动视图, 所以对于vue来说组件间的数据通信非常重要。vue组件通信也是面试中常问到的面试题。熟练的掌握vue组件通信对于我们来说至关重要。那么组件之间如何进行数据通信的呢?本篇文章较长,将比较全面的讲述各种通信方式,应该是最全的关于vue通信的文章了,若有遗漏,欢迎指出。下面将围绕父子通信组件非父子组件通信展开,将配上代码讲解、结果示例图。

如果对你有用,敬请收藏,也是对原创作者的鼓励!

首先,我们要明白,vue组件通信主要分为:

  • 父子组件之间通信(又具体分为父组件向子组件传值、子组件向父组件传值): props$emit.syncv-modelref$parent$children
  • 父组件跟孙子组件的通信: $attr$listenersprovideinject
  • 非父子组件之间通信: eventbusvuex

一、父子组件之间通信

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>
WechatIMG299.png

props还可以设置默认值,如下所示:

//child.vue
props: {
        title: {
            type: String,
            default: '默认-工作日'
        },
        list: {
            type: Array, 
            default: function () {
                // 对象或数组Array默认值必须从一个工厂函数获取
                return ['上班搬砖','喝快乐肥宅水','继续码']
            }
        }
    },

这时候,如果你在父组件parent.vue中没传 title 跟 list 的话,将展示如下:

WechatIMG300.png

props 的 type 值为:StringNumberBooleanArrayObjectDateFunctionSymbol

注意:父级 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>
WechatIMG301.png

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>

实现效果

1.gif

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>
1.gif

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>
2.gif

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>

得到的效果如下:

3.gif

注意:

  1. this.$parentthis.$children返回的值不一样,this.$children 的值是数组,而this.$parent是个对象
  2. 在#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>

实现效果如下:

4.gif

注意: $attr包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

inheritAttrs

inheritAttrs 默认是true, 如果没设置为false的话,父作用域的不被认作 props 的特性绑定将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。如果我们设置 inheritAttrs:false,这些默认行为将会被去掉。是不是觉得特别晦涩难懂,别慌~ 看下面的两张对比图,相信你就懂了!

WechatIMG311.png
WechatIMG312.png

2. provideinject

provide/ inject 是vue2.2.0新增的api, 简单点来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。

provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。 provideinject允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 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.vuecomp1.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这里就不赘述了,建议直接移步官方文档

总结

下面,再来一张思维导图来阐述通信的主要方式。

WechatIMG313.png

写在最后

这篇文章断断续续写了一周,主要是这段时间看了些vue3.0,觉得很多东西都是共通的。便想对自己最熟悉的vue2.0做个总结。毕竟,vue3.0composition写法对代码改动还是很大的。重构代码需要花时间,目前还是以vue2.0为主吧。

发表回复