Vue组件通信事件总结

3,974 阅读6分钟

在Vue的项目开发里面,组件之间的通信实在是太常见了,虽说Vue已经出了很久了,但我接触它的时间仍不是很多,趋于业务开发,有时候会踩到一些坑,也是初级开发者很容易遇到的问题,当然网上也都有很多解决方案的文章,不过每次一遇到问题就百度一下的习惯并没有让自己系统的理解Vue里面的通信机制。比如.sync这个修饰符,使用ElementUI的Dialog组件常见到,但一直是拿来即用,没有思索过具体用处,遂总结整理一下在Vue中组件之间的通信机制,方便日后开发中按照场景使用更合适的API,而不是只会$emit$on这一种用法。

本篇文章主要参考学习了掘金的一篇文章,指路➡️ vue组件通信全揭秘 , 感觉讲的很详细,很多地方给我一种恍然大悟的感觉,非常感谢。

data 和 props (同步变化)

在vue中,我们最常见的恐怕莫过于data了,vue通过{{}}绑定动态数据,当data的值发生变化时,可以同步的更新到视图中。

而props常用于数据传递,比如子组件想要使用父组件中的数据,就可以通过props接收父组件的值。而父组件使用v-bind来把数据传递给子组件。当父组件中的数据发生改变时,子组件中对应的视图层也会相应的更新。

以上两者里的每个数据都是行为操作需要的数据或者模板 view 需要渲染的数据,一旦其中一个属性发生变化,则所有关联的行为操作和数据渲染的模板上的数据同一时间进行同步变化。

// 父组件A
<template>
   <div>
      <p>test: {{test}}</p>
      <p>msgData: {{test}}</p>
      <ComponentB :msg='msgData'></ComponentB>
   </div>
</template>
<script>
import ComponentB from './ComponentB.vue'
   export default {
     data () {
       return  {
          test: '我是测试数据',
          msgData: '我是传递给子组件的数据'
       }
     },
     components : {
       ComponentB
     }
   }
</script>


// 子组件B
<template>
   <div>
      <p>msgData: {{msgData}}</p>
   </div>
</template>
<script>
   export default {
      name: 'ComponentB',
      props: [ 'msgData'],
   }
</script>

data 和 props 的不同

  • data在任何情况下改变数据类型和数据结构,都能同步反应到view层。
  • 而props一旦初始化后,在数据传递时不能改变它的数据类型;因为Vue时单向数据流,通过父组件传递的数据,在子组件中不能修改,只能改变传递源中的数据。

虽然都会影响视图层的数据,不过props跟data的使用还是有些区别的。下面就来说说我在代码中遇到过的有关props的坑(也可能是常见的错误):

Props踩过的坑

  1. 改变props数据报错:

在子组件中不可以直接修改props中的数据,这也正是vue中单向数据流的特性,父组件向下传递的数据,如果父组件发生改变,会同步的反应在子组件中;但子组件的数据却不会同步影响上一层,就像水一样,只能往下走。(当然可以通过其他方式来影响父组件的数据,比如事件)

  1. 如果实在是想要改动props传递过来的数据怎么办?

虽说props的数据是不能显式的直接改变,但我们可以通过间接的方式来修改数据。

  • 1、利用data的动态性,我们可以将props的数据转存到data中,这样就可以通过修改data中的数据来更改对应的视图层啦
props: ['msg'],
data() {
  return { 
    myMsg: this.msg 
  }
}
  • 2、可以转存到计算属性computed中
props:['msg']
computed : {
   myMsg () {
       return this.msg;
   }
}

不过因为对象和数组是引用类型,指向同一个内存空间,所以不要通过computed来对父组件传递来的引用类型数据进行计算过滤,改变数据会影响到父组件的状态。

props对象传递简写

  • 当父组件传递给子组件的数据数量很多,可以通过一个对象传递:
<!--父组件:-->

<!--html部分: -->
<demo v-bind='msg'></demo>

<!--js部分:-->
data () {
    return {
        msg : {a:1,b:2}
    }
}


<!--子组件:-->
props: ['a','b']


<!--父组件传递的过程相当于如下:-->
<demo :a='a' :b='b' ></demo>

$emit$onv-on

上面的props中说过,子组件不能直接改变父组件传递进来的数据,除了通过data或computed转存的间接方式来改变传递的数据外,还有另一种方式,就是$emit事件。子组件用通信的方式来告知父组件进行数据的更新。

  • $on(eventName, callback) 监听事件, 第二个参数是回调函数,回调函数的参数为$emit传递的数据内容
  • $emit(eventName, [...arg]) 触发事件, 第二个参数会传递给on监听器的回调函数
  • v-on则是使用在父组件标签中的,可以对其子组件的$emit监听

.sync 双向绑定

当然,v-model也是用于双向绑定的,不过因为太常见了,暂时没有做进一步的分析。

  • .sync的功能是:当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定。
  • 使用方式: <comp :foo.sync="bar"></comp>
  • 相当于:<comp :foo="bar" @update:foo="val => bar = val"></comp>
  • 当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件: this.$emit('update:foo', newValue)
<!--常见的如饿了么组件中的:current-page.sync-->
<el-pagination 
layout="prev, pager, next" 
:total="meta.total" 
@current-change="load" 
:current-page.sync="meta.current_page" 
background>
</el-pagination>
    
<!--js部分:-->
props: {
    meta: {
      type: Object,
      required: true,
    },
  },

再如,饿了么组件Dialog:

<el-dialog
  title="提示"
  :visible.sync="dialogVisible"
  width="30%"
  :before-close="handleClose">
  <span>这是一段信息</span>
  <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
  </span>
</el-dialog>

$attrs,$listeners, 深层次数据传递(Vue v2.4及以上使用)

当数据层次变多后,已经不仅包括父子组件这么简单的关系了,可能会有第三层、第四层组件,复杂的情况下可以用vuex,但简单情况下只是偶尔有多层组件关系时,可以选择使用$attrs,这种方式 组件封装用的较多

先来解释一下这两个属性:

  • $attrs: 包含了父作用域中不作为 props 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 ( class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件 —— 在创建高级别的组件时非常有用。
  • $listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件 —— 在创建更高层次的组件时非常有用

事实上,你可以把 $attrs$listeners 比作两个集合,其中 $attrs 是一个属性集合,而 $listeners 是一个事件集合,两者都是 以对象的形式来保存数据 。

比如我们有三个组件A,B,C,分别是父组件、子组件、孙组件,如果想从A组件传递数据msg给C组件:

常规做法是一层一层使用props来传递,但实际上B并不需要A传递的这个msg,除了这种思路外,还有一种写法,

A组件:

<template>
  <div class="home">
    这里是首页
    <ChildB
    :one="one"
    :two="two"
    @clickOne.native="triggerOne"
    @clickTwo="triggerTwo"
    ></ChildB>
  </div>
</template>

<script>
import ChildB from '../components/ChildB.vue';

export default {
  name: 'home',
  components: {
    ChildB,
  },
  data() {
    return {
      one: 'data 1',
      two: 'data 2',
    };
  },
  methods: {
    triggerOne() {
      console.log('triggerOne');
    },
    triggerTwo() {
      console.log('triggerTwo');
    },
  },
};
</script>

B组件:

<template>
  <div>
    B 组件
    {{one}}:
    <ChildC></ChildC>
  </div>
</template>

<script>
import ChildC from './ChildC.vue';

export default {
  components: {
    ChildC,
  },
  props: ['one'],
  created() {
    console.log('$attrsB:', this.$attrs);
    console.log('$listenersB:', this.$listeners);
  },
};
</script>

C组件:

<template>
  <div>
    我是 C 组件
  </div>
</template>

<script>

export default {
  created() {
    console.log('$attrsC:', this.$attrs);
    console.log('$listenersC:', this.$listeners);
  },
};
</script>

此时控制台打印出:

$attrsB: {two: "data 2"}

$listenersB: {clickTwo: ƒ invoker()}

$attrsC:{}

$listenersC: {}

这时候B组件可以通过this.$attrsthis.$listeners,直接访问A组件的属性和事件,注意,这个属性是非props中的才能通过这种方式访问,而带.native修饰器(监听组件根元素的原生事件)的事件也同样访问不到。

注意,此时C组件使用this.$attrsthis.$listeners还是访问不到A组件的属性和事件的。

我们可以通过在B组件中调用C组件的过程中使用v-on="$listeners" 一级级地往下传递,此时C组件中就可以访问到A组件的事件了

<!--B组件:-->
<ChildC v-on="$listeners"></ChildC>


<!--C组件-->
<template>
  <div>
    我是 C 组件
  </div>
</template>

<script>

export default {
  created() {
    console.log('$attrsC:', this.$attrs);
    console.log('$listenersC:', this.$listeners);
    this.$listeners.clickTwo();
  },
};
</script>

输出:

$attrsC: {}
$listenersC: {clickTwo: ƒ}
triggerTwo

还记得我们上面的two属性嘛?我们给B组件传递的props属性只有one没有two,那这个时候two属性去哪里了呢?

  • 组件编译之后会把非 props 属性当成原始属性对待,从而添加到DOM元素(HTML标签上),如这里的two属性,在编译成html后,two变成了一个div标签上的属性: <div two="data 2">

A组件到C组件的数据传递 上面简单介绍了下$attrs,和$listeners的基础用法,回到三个组件传递数据这一点上,我们看一下多层级组件到底怎么借助$attrs,和$listeners来实现数据通信的。

A组件不变,

B组件:

<template>
  <div>
    B 组件
    <p>props: {{one}} </p>
    <p>$attrs: {{$attrs}} </p>
    <p>$listeners: {{$listeners}}  </p>

    <ChildC v-on="$listeners" v-bind="$attrs"></ChildC>
  </div>
</template>

<script>
import ChildC from './ChildC.vue';

export default {
  components: {
    ChildC,
  },
  inheritAttrs: false,
  props: ['one'],
  created() {
    console.log('ComponentB', this.$attrs, this.$listeners);
  },
};
</script>

C组件:

<template>
  <div>
    我是 C 组件
    <p>props: {{two}} </p>
    <p>$attrs: {{$attrs}} </p>
    <p>$listeners: {{$listeners}}  </p>
  </div>
</template>

<script>

export default {
  props: ['two'],
  created() {
    console.log('ComponentC', this.$attrs, this.$listeners);
  },
};
</script>

此时控制台上打印出:

ComponentB {two: "data 2"} {clickTwo: ƒ}
ComponentC {} {clickTwo: ƒ}

可以发现,C组件可以直接通过props来继承A组件中的two属性,不过这是在B组件直接往下传递v-bind="$attrs"的前提下。

代码里有一段 inheritAttrs: false, 这就是为了禁止非props的属性添加到DOM元素上,现在打开浏览器的调试窗口,就不会看见two属性了

$children / $parent 方式

这种方式适用于木偶组件,何为木偶组件?就是为了业务页面进行拆分而形成的组件模式。比如一个页面,可以分多个模块,而每一个模块与其余页面并没有公用性,只是纯粹拆分。

这种情况下的组件明确的知道自己的父组件是哪一个,且不需要复用,就可以通过 $children / $parent 方式来进行通信了。

$parent

比如A组件种有one和two两个属性,我们不需要通过v-bind来传递,就可以直接在A的子组件中通过this.$parent.onethis.$parent.two 来获取。并且还可以改变父组件的数据:this.$parent.one = '父组件one被改了',

同样事件也可以通过这种方式调用,父组件定义parentMethods()方法,

子组件调用:

this.$parent.parentMethods()

$children

$children$parent 相反,是父组件拿到子组件的实例,需要注意的是,$children 是以一个数组的形式包裹。

this.$children.forEach(item => {
    console.log(item);
})

ref / refs

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据。获取 Dom 元素就是为了进行一些 Dom 操作,需要注意的一点就是,要在 Dom 加载完成后使用,否则可能获取不到。

使用场景:

  • 父元素的某个状态改变,需要子组件进行 http 请求更新数据

ref的三种使用方式:

1、ref 加在普通的元素上,用this.ref.name 获取到的是dom元素

<input type="text" ref="input1"/>

<!--JS部分改变数据-->
this.$refs.input1.value ="test"; //this.$refs.input1 减少获取dom节点的消耗

2、ref 加在子组件上,用this.ref.name 获取到的是组件实例,可以使用组件的所有方法。

<!--父组件:-->
<child ref="childComponent"></child>
<!--JS调用:-->
this.$refs.childComponent.getLocalData()

<!--子组件定义方法:-->
getLocalData(){
    // ...
}

3、如何利用 v-for 和 ref 获取一组数组或者dom 节点

<ul v-for="(item, key) in list" :key="key">
    <li ref="item">姓名:{{item.name}}, 年龄: {{item.age}}</li>
</ul>
    
<!--JS部分   -->
data() {
    return {
        list: [
        { name: 'armor', age: 24 },
        { name: 'zhong', age: 23 },
        { name: 'lili', age: 12 },
        { name: 'yangyang', age: 29 },
      ],
    }
},
created() {
    console.log('refs: ', this.$refs);
}

这里打印的结果是:

refs:  {
    item: [
    0: li,
    1: li,
    2: li,
    3: li
    ]
}

每一个li就是对应的DOM元素,然后就可以对其进行操作了。

中央事件eventBus (非父子关系的同级组件)

如First组件与同级组件Second相互通信,这两个没有共同父组件。

  1. 定义一个中央事件实例:
import Vue from 'vue'

export default new Vue()
  1. First和Second组件都需要引入这个中央事件实例
import Bus from './bus.js'

<!--First组件触发:-->
Bus.$emit('fromFirst', '来自A的组件')

<!--B组件监听-->
Bus.$on('fromFirst', ( Amsg )=> {
    this.Bmsg = Amsg
    console.log('同级组件交互成功')
})

小结

其实Vue的通信事件不止上面几种,还有vuex中的数据交互,但是vuex比较复杂,一般不是很复杂的业务其实用不上vuex,用上面几种方式实现组件之间的通信足矣。

文章参考:

vue组件通信全揭秘