Vue组件通信方式的使用总结和对比

1,036 阅读8分钟

前言:组件通信是最基础常用也是非常重要的知识,项目组件化,功能模块化都离不开组件之间的通信传值。本篇系统来整理一下用的比较多的7种方式。

参考该文章稍作修饰,附上原文链接:juejin.cn/post/688770…

组件通信种类

提到组件通信,无非是从以下几个方面来介绍:

  • 父子组件传值
  • 兄弟组件传值
  • 跨级组件间传值

常用方法

Props / $emit

这是我们最常用的方式,它能够满足基本和绝大多数需求。

Props(父传子):单向数据流由父到子。props可以是数组或对象,用于接收来自父组件通过v-bind传递的数据。

emit(子传父) :在父子组件通信时,我们通常会使用$emit来触发父组件v-on在子组件上绑定相应事件的监听。

  • 当props为数组时,直接接收父组件传递的属性;
  • 当 props 为对象时,可以通过type、default、required、validator等配置来设置属性的类型、默认值、是否必传和校验规则。

示例

parent.vue:

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <child :message="message" @receiveMsg="receiveMsg" :num="100"></child>
    <p>这是子组件传来的值:{{messageFromChild}}</p>
  </div>
</template>

<script>
  import child from './child'
  export default {
    components: {
      child
    },
    data() {
      return {
        message: 'hello',  //要传递给子组件的内容
        messageFromChild: ''
      }
    },
    methods: {
      receiveMsg(value) {
        this.messageFromChild = value
      }
    }
  }
</script>

child.vue:

<template>
  <div class="child">
    <h3>我是子组件</h3>
    <p>这是父组件传来的值:{{message}}</p>
    <el-button type="primary" @click="send">发送</el-button>
  </div>
</template>

<script>
  export default {
    // props: ['message'],  //props接收父组件传递的值
    props:{
      message:{
        type:String,
        require:true
      },
      num:{
        //校验:如果传递的值小于50正确,大于50会报错
        validator(val){
          return val < 50
        }
      }
    },
    data() {
      return {

      }
    },
    methods: {
      send() {
        //通过$emit触发receiveMsg事件,从而调用父组件中receiveMsg方法
        this.$emit('receiveMsg', this.message + 'fromChild')
      }
    }
  }
</script>

注意

  • Props传递来的数据是不能直接修改的。
  • 如果传到子组件中的值在父组件会被改变,需要放到computed中使用或者watch中监听
  • 如果传到子组件中的值要做一些操作,不能直接修改,可以借助get/set。或者可以把对数据的操作写到watch中深拷贝后监听处理。

v-slot

v-slot在 template 标签中用于提供具名插槽或需要接收 prop 的插槽,如果不指定 v-slot ,则取默认值 default 。

使用场景

v-slot小编使用到比较少,用slot的场景基本是自定义dialog弹框和table中template的slot-scope等场景。使用方法和原理是类似的。

代码

parent.vue:

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <child>
      <template v-slot:child>
        <!-- 插槽要显示得内容 -->
        {{message}}
      </template>
    </child>
  </div>
</template>

<script>
  import child from './child'
  export default {
    components: {
      child
    },
    data() {
      return {
        message: 'hello',
      }
    },
  }
</script>

child.vue:

<template>
  <div class="child">
    <h3>我是子组件</h3>
    <p>这是父组件传来的值:
      <!-- 展示父组件通过插槽传递得message -->
      <slot name="child"></slot>
    </p>
  </div>
</template>

refs/refs/parent/children/children/root

当父组件需要获取子组件的一个数据但是子组件并不知道或者说在父组件需要的时候提供给父组件,也就是没有按钮等触发事件的时候,就会用到ref。

<child ref="child"></child>
  • 如果ref用在子组件上指向的是组件实例,通过$ref可以获取到子组件里定义的属性和方法;
  • 如果ref在普通DOM元素上使用指向的是DOM元素,通过$ref获取到DOM的属性集合,类似于jq选择器;

父组件在标签中定义ref属性,直接调用this.$refs.child就可以调用整个子组件child中所有的属性和方法。

使用场景

用于表单会较多,表单校验、提交等,当然上面提到的无按钮触发事件间的消息传递就可以用到$ref。

示例(针对parentparent和children)

parent.vue:

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <child></child>
    <p>这是子组件传来的值:{{childObj.messageFromChild}}</p>
  </div>
</template>

<script>
  import child from './child'
  export default {
    components: {
      child
    },
    data() {
      return {
        message: 'hello',  //要传递给子组件的内容
        childObj: {}
      }
    },
    mounted() {
      console.log(this.$children)
      this.childObj = this.$children.find(child => {
        //$options.name获取到对应name得child实例
        return child.$options.name === 'child'   //name属性在原型链上
      })
    },
  }
</script>

child.vue:

<template>
  <div class="child">
    <h3>我是子组件</h3>
    <!-- 显示父组件中message得值 -->
    <p>这是父组件传来的值:{{$parent.message}}</p>
  </div>
</template>

<script>
  export default {
    name: 'child',   //通过name来获取对应子组件实例
    data() {
      return {
        messageFromChild: '我是子组件的message'   //父组件通过this.$children获取到子组件实例的值
      }
    },
  }
</script>

rootroot和parent的区别

都能访问到父组件的属性和方法,不同的是如果是多级子组件,parent只能访问到最近一级的父组件,parent只能访问到最近一级的父组件,root访问到的是根组件。

attrs/attrs/listener

$attrs:

也就是attrs中包含的是没有被声明为props的属性,如果某个属性props中接收了,那么attrs中包含的是没有被声明为props的属性,如果某个属性props中接收了,那么attrs中就会剔除掉该属性。

当我们创建了一个组件,要接收 param1 、param2、param3 …… 等数十个参数,如果通过 props,那就需要通过props: ['param1', 'param2', 'param3', ……]等声明一大堆。如果这些还需要往更深层次的子组件传递,那将会更加麻烦。而使用 attrs,直接通过attrs ,直接通过attrs.param1、$attrs.param2……就可以使用,而且向深层子组件传递上面也十分方便。

listeners:包含了父作用域中的 v-on 事件监听器。它可以通过 v-on="listeners"传入内部组件——在创建更高层次的组件时非常有用,这里在传递时的使用方法和listeners" 传入内部组件——在创建更高层次的组件时非常有用,这里在传递时的使用方法和 attrs 十分类似。

使用场景

适用于批量传递数据。

代码

A组件嵌套B组件,B组件嵌套C组件,也就是A组件是B组件的父组件,B组件是C组件的父组件。A为1级组件,B为2级组件,C为3级组件.具体流程:

  • 父向子传值:组件A通过:messageFromA="message"将 message 属性传递给组件B,组件B通过$attrs.messageFromA获取到组件A的 message;
  • 跨级向下传值:组件A通过:messageFromA="message"将 message 属性传递给组件B,组件B再通过 v-bind="attrs"将其传递给组件C,组件C通过attrs"将其传递给组件C,组件C通过attrs.messageFromA获取到组件A的 message;
  • 子向父传值:组件A通过@keyup="receive"在子孙组件上绑定keyup事件的监听,组件B在通过v-on="$listeners"来将 keyup 事件绑定在其 input 标签上。当组件B input 输入框输入时,触发组件A的receive回调,将组件B的 input 输入框中的值赋值给组件A的 messageFromChild ,从而实现子向父传值。
  • 跨级向上传值:组件A通过@keyup="receive"在子孙组件上绑定keyup事件的监听,组件B在通过 将其继续传递给C。组件C在通过v-on="$listeners"来将 keyup 事件绑定在其 input 标签上。当组件C中 input 输入框输入,触发组件A的receive回调,将组件C的 input 输入框中的值赋值给组件A的 messageFromChild ,实现跨级向上传值。

A.vue:

<template>
  <div class="parent">
    <h3>我是A组件</h3>
    <input type="text" v-model="message">
    <!-- 把message传递给子孙组件,监听子孙组件的keyup事件 -->
    <B :messageFromA="message" @keyup="receiveMsg"></B>
    <p>这是{{child}}组件传来的值:{{messageFromChild}}</p>
  </div>
</template>

<script>
  import B from './B'
  export default {
    components: {
      B
    },
    data() {
      return {
        a:'我是属性a',
        b:'我是属性b',
        message: '',  //要传递给子组件的内容
        messageFromChild: '',
        child: ''
      }
    },
    methods: {
      // 监听子孙组件中key事件回调,将输入框中的内容赋值给该组件中的变量messageFromChild
      receiveMsg(e) {
        console.log(e)
        this.child = e.target.name
        this.messageFromChild = e.target.value
      }
    }
  }
</script>

B.vue:

<template>
  <div class="child">
    <h3>我是B子组件</h3>
    <input name="B" type="text" v-model="message" v-on="$listeners">
    <!-- 接收父组件传来的message -->
    <p>这是A组件传来的值:{{$attrs.messageFromA}}</p>
    <!-- 将A组件传递的attrs传递给C组件,C组件可通过$attrs.messageFromA接收值,且把A组件中keyup的监听回调传递给C组件 -->
    <C v-bind="$attrs" v-on="$listeners"></C>
  </div>
</template>

<script>
  import C from './C'
  export default {
    // props:['messageFromA'],
    components: {
      C
    },
    data() {
      return {
        message: ''
      }
    },
    created(){
      console.log(this.$attrs)
    }
  }
</script>

C.vue:

<template>
  <div class="child">
    <h3>我是C子组件</h3>
    <input name="C" type="text" v-model="message" v-on="$listeners">
    <!-- 接收父组件传来的message -->
    <p>这是A组件传来的值:{{$attrs.messageFromA}}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        message: ''
      }
    }
  }
</script>

Provide/inject

provide:是一个对象,或者是一个返回对象的函数。该对象包含可注入其子孙的子孙的属性和属性值。 injcet:一个字符串数组,或者是一个对象。当其为字符串数组时,使用方式和props十分相似,只不过接收的属性由data变成了provide中的属性。当其为对象时,也和props类似,可以通过配置default和from等属性来设置默认值,在子组件中使用新的命名属性等。

使用场景

小编使用该方法的场景就是实现无闪烁刷新页面。

此处附一个使用链接,就不贴代码了:blog.csdn.net/weixin_4388…

eventBus(事件总线)

通过注册一个新的Vue实例,调用这个实例的emitemit和on等来监听和触发这个实例的事件,通过传入参数从而实现组件的全局通信。

全局:Main.js

window.eventBus = new Vue();

当项目过大时,最好将事件总线抽象为单个文件,将其导入到需要使用的每个组件文件中。这样,它不会污染全局命名空间。

局部:bus.js,使用时通过**import引入

import Vue from 'vue'
export const Bus = new Vue()

在vue页面销毁时,同时移除eventBus事件监听【EventBus.$off('事件名')】。刷新页面,与之相关的eventBus就会被移除。

该方式在刚刚接触vue,还不会用vuex的时候有使用过,后期基本没再用过。

代码

parent.vue:

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <el-button type="primary" @click="send">父传子</el-button>
    <child></child>
    <child2></child2>
    <p>这是子组件传来的值:{{messageFromChild}}</p>
  </div>
</template>

<script>
  import child from './child'
  import child2 from './child2'
  export default {
    components: {
      child,
      child2
    },
    data() {
      return {
        message: '这是父组件传来的值',  //要传递给子组件的内容
        messageFromChild: ''
      }
    },
    mounted() {
      // $on接收传递来的值
      eventBus.$on("sendMsgFromChild", (data) => {
        console.log(data);
        this.messageFromChild = data;
      })
    },
    beforeDestory() {
      eventBus.$off('sendMsgFromChild');
    },
    methods: {
      send() {
        eventBus.$emit("sendMsg", this.message);   //$emit传递给子组件
      }
    }
  }
</script>

child.vue:

<template>
  <div class="child">
    <h3>我是子组件</h3>
    <p>这是父组件传来的值:{{messageFromParent}}</p>
    <el-button type="primary" @click="send">子传父</el-button>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        messageFromParent: ''
      }
    },
    mounted() {
      // $on接收传递来的值
      eventBus.$on("sendMsg", (data) => {
        console.log(data);
        this.messageFromParent = data;
      })
    },
    beforeDestory() {
      eventBus.$off('sendMsg');
    },
    methods: {
      send() {
        eventBus.$emit("sendMsgFromChild", '这是子组件传来的值');   //$emit传递给父组件
      }
    }
  }
</script>

child2.vue:

<template>
  <div class="child">
    <h3>我是子组件2</h3>
    <p>这是父组件传来的值:{{messageFromParent}}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        messageFromParent: ''
      }
    },
    mounted() {
      // $on接收传递来的值
      eventBus.$on("sendMsg", (data) => {
        console.log(data);
        this.messageFromParent = data;
      })
    },
    beforeDestory() {
      eventBus.$off('sendMsg');
    },
  }
</script>

原理

采用发布订阅模式,实现emitemit和on方法。

此处简单介绍发布订阅模式——当一个状态发生改变的时候,所有关注或者依赖该状态的对象都可以收到对应的通知;其实就是给发布者添加缓存列表,以数组的形式存放回调函数。发布消息时,发布者遍历缓存列表,依次触发存放的订阅者的回调函数执行。

这里对应过来就是emit负责发布消息,emit负责发布消息,on负责订阅消息。

vuex

Vuex创造了一个全局的vue实例(new Vue),可以更改vue实例的数据,用于多个组件中数据共享、数据缓存等,来集中管理所有组件的状态,可以把vuex理解为一个仓库。仓库中的货物就是各个对象,其中state是存放数据的地方,这里的数据都是响应式的。

使用场景

Vuex一般用于中大型项目中对状态进行管理,如果是一些较为简单的小型应用,使用prop属性或者事件来完成父子组件之间通信或者eventBus完成兄弟组件通信即可,vuex更多地是用于解决跨组件通信以及作为数据管理中心集中存储和管理数据状态的。

5种属性

state / getter / mutation / action / module

State:唯一数据源,和vue实例中data遵循相同规则;

Getters:store的计算属性,返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。通过store.getters.xxx来访问对应值。

Mutation:通过提交mutation来更改store中的状态,用store.commit方法触发。

Action:类似mutation,区别在于action提交的是mutation,而不是直接修改状态,action可以包含任何异步的操作。

Module:如果项目比较复杂,所有状态集中在一起会很庞大臃肿,不好管理。此时就可以将store分割成模块,使用module来划分,将相似的内容存储在一个模块中去统一管理。

Mutation和action区别:

MutationAction
直接修改状态提交的是mutation,不是直接修改状态
只能是同步操作可以包含任意异步操作
dispatch提交 第一个参数是statecommit提交 第一个参数是context,其中包含state/rootState/commit/dispatch/getters/rootGetters

注意事项

  • 从vuex中获取到的数据,不能直接修改,需要浅拷贝一份再进行修改,否则会报错。
  • Vuex只是做了暂时的数据状态共享,在页面刷新后数据会消失,所以一般需要配合sessionStorage和localStorage存储数据实现一些效果。之前项目中也有借助插件vuex-persist和vuex-along来解决,这两插件的原理也是将数据存储到localStorage,只是直接调用已经封装好的方法而不需要手动存取了。

总结

父子通信:以上7种方式都可以实现父子组件通信,其中props/$emit最为常用,满足我们基本需求。

  • v-slot可实现父子单向通信,父向子传值。实现可复用组件时,向组件中传入DOM节点,html等内容以及某些组件库的表格值进行二次处理,可优先考虑v-slot
  • refs/refs/parent/children/children/root实现父子组件双向通信,$root可以实现跟组件实例向子孙组件跨级单向传值。在父组件没有传递值或者通过v-on绑定监听时,父子间想要获取彼此属性或方法可以考虑使用这些。

兄弟通信:eventBus和vuex

  • eventBus适用于规模不大的情况下,实现全局的事件监听。但是要注意,使用完毕destory掉,避免全局污染和内存泄漏问题。
  • vuex适用于规模比较大的情况,用来集中管理全局组件状态。

跨级通信attrs/attrs/listener provide/inject eventBus vuex

  • attrs/attrs/listener可实现跨级双向通信,既能很容易获取到传入的属性,绑定监听,也能方便的向下级子组件传递值。适用于构建高级组件。
  • Provide/inject实现的是跨级单向通信,向子孙组件注入依赖。适用于实现高级组件、创建组件库。

小编平时工作使用最多的就是props/emitvuexemit,vuex,refs,其余用的频率较少,都是基于某种场合偶尔使用,见解还不算很深,之后会继续研究再做剖析,如有问题,欢迎指证!