Vue组件通讯事件总结

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

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

data 和 props (同步变化)

在vue中,咱们最多见的恐怕莫过于data了,vue经过{{}}绑定动态数据,当data的值发生变化时,能够同步的更新到视图中。vuex

而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的坑(也多是常见的错误):bash

Props踩过的坑数据结构

  1. 改变props数据报错:

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

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

虽然说props的数据是不能显式的直接改变,但咱们能够经过间接的方式来修改数据。函数

  • 一、利用data的动态性,咱们能够将props的数据转存到data中,这样就能够经过修改data中的数据来更改对应的视图层啦
props: ['msg'],
data() {
  return { 
    myMsg: this.msg 
  }
}
复制代码
  • 二、能够转存到计算属性computed中
props:['msg']
computed : {
   myMsg () {
       return this.msg;
   }
}
复制代码

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

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的三种使用方式:

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

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

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

二、ref 加在子组件上,用this.ref.name 获取到的是组件实例,可使用组件的全部方法。

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

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

复制代码

三、如何利用 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: 'abtion', 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组件通讯全揭秘

相关文章
相关标签/搜索