组件之间的传值通信

6/24/2020 Vue

# 方式 1: props/$emit/$on

# 1.父组件向子组件传值

  • 父组件通过 props 给子组件传值
  • 子组件通过定义 props.list 接收指定的父组件传过来的值
  • 子组件通过 $emit() 方法给父组件回调方法
  • 父组件通过 v-on/@ 来监听对应的 emit事件,回调中的参数就是子组件传回来的参数

父组件

<template>
  <div class="parent">
    <todo :list="list" @clickItem="clickItem"></todo>
  </div>
</template>

<!-- 省略JS不写了 -->
1
2
3
4
5
6
7

子组件:

<template>
  <ul>
    <li v-for="(item,index) in list" :key="index" @click="$emit('clickItem',item)">
      {{item}}
    </li>
  </ul>
</template>

<script>
  export default {
    name: 'Todo',
    props: {
      list: {
        default: [],
        type: Array
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2. 兄弟/跨级组件传值

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子兄弟跨级

当我们的项目比较大时,可以选择更好的状态管理解决方案 vuex。

2.1 初始化:

首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它。我们可以通过两种方式来处理。先来看第一种,新创建一个 .js 文件,比如 event-bus.js

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
1
2
3

或者在 main.js 初始化 EventBus也可以

// main.js
Vue.prototype.$EventBus = new Vue()
1
2

2.2 在 a 组件发出一个事件

<!-- A.vue -->
<template>
  <button @click="sendMsg()">发送消息</button>
</template>

<script>
  import { EventBus } from '../event-bus.js'
  export default {
    methods: {
      sendMsg() {
        EventBus.$emit('aMsg', '来自A页面的消息')
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2.3 在 b 组件接收 a 发出的事件

<!-- A.vue -->
<template>
  <p>{{msg}}</p>
</template>

<script>
  import { EventBus } from '../event-bus.js'
  export default {
    data() {
      return {
        msg: ''
      }
    },
    mounted() {
      EventBus.$on('aMsg', msg => {
        // A发送来的消息
        this.msg = msg
      })
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

2.4 小结一下

EventBus 主要用到 2 个方法:

// 发送消息
EventBus.$emit('事件名', '传递的参数/对象')

// 监听接收消息
EventBus.$on('事件名', () => {
  // 接收参数的回调函数
})
1
2
3
4
5
6
7

but!这样会很大的隐患

  • vue 是单页应用,如果你在某一个页面刷新了之后,与之相关的 EventBus 会被移除,这样就导致业务走不下去

  • 还要就是如果业务有反复操作的页面,EventBus 在监听的时候就会 触发很多次,也是一个非常大的隐患。

  • 这时候我们就需要好好处理 EventBus 在项目中的关系。通常会用到,在 vue 页面销毁时,同时移除 EventBus 事件监听

2.5 移除监听事件

import { eventBus } from './event-bus.js'

EventBus.$off('aMsg') // 使用 $off 移除对 aMsg的监听
EventBus.$off() // 移除所有事件频道,不需要添加任何参数 。
1
2
3
4

# 方式 2:vuex

关于 vuex 的介绍和基础用法

通过 vuex 的方法,触发值修改,其余组件会进行同步更新

# 方式 3:$attrs/$listeners

末尾有完整的 demo,下面描述的都是代码片段

# $attrs

官方文档:$attrs (opens new window)

  • 继承所有的父组件属性除了 prop 传递的属性、class 和 style ),一般用在子组件的子元素上

实际用途

解决方法 1:children 多接收一个 age 属性,继续传递给 grandson

缺点:冗余!明明 children 无须用到 age,这样对组件定义 props 是极其不友好的

解决方法 2:使用 vuex。

缺点:太大材小用了。vuex 内部那么复杂的东西,用来解决跨级数据传递问题,这是对性能的浪费(虽然现在手机/电脑性能都不差)

像这种时候,$attrs 就非常有用

看个栗子:

<!-- parent组件,父组件 -->
<div class="parent">
  <children name="Jioho" age="111" sex=""></children>
</div>

<!-- children组件 -->
<div class="children">
  <grandson :name="name" v-bind="$attrs"></grandson>
</div>
<script>
  export default {
    props: ['name']
  }
</script>

<!-- grandson组件 -->
<div class="grandson">
  <div>name:{{name}}</div>
  <div>age:{{age}}</div>
  <div>sex:{{$attrs['sex']}}</div>
</div>
<script>
  export default {
    props: ['name', 'age']
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

注意上面的代码:

  • 最后 grandson 可以拿到 2 个 props。并且 sex 也可以正常输出
  • children 组件只接收了 name 属性

1. 先看 children 组件

定义了一个参数 props:name 那么,childred 里面的$attrs = {age="Jioho",sex="男"}

印证了他的定义

继承所有的父组件属性除了 prop 传递的属性、class 和 style ),一般用在子组件的子元素上

如果这时候 children 的 props 多接收一个 sex。即:props:['name','sex']。那 $attrs 就只有一个age属性

1.1 小结:

  • 注意字眼:继承、父组件传递的值,不用显示定义(props) 也可以拥有。只是这个存放在了 $attr

  • 用一条公式来带过一下: $attrs = 父组件传递下来的参数 - 子组件定义的 props - class - style 除去 子组件中的 props,class,style 剩下的都可以在$attr找到。

  • 在高阶组件中,如果有非常多不确定的参数要继续传递给子组件,就可以用上 $attrs

详细的 demo 代码可以在末尾找到,自己修改一下传递的参数,修改一下 props 便能理解

# $listeners

官方文档:$listeners (opens new window)

  • 它是一个对象,里面包含了作用在这个组件上的所有监听器,

  • 你就可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件

  • $attrs 很相似,跨级组件的事件通信。

看个栗子:

<!-- parent 组件 -->
<div class="parent">
  parent
  <children
    :name="name"
    :age="age"
    :sex="sex"
    @testClick="testClick"
    @testChildClick="testChildClick"
  ></children>
</div>

<script>
  export default {
    methods: {
      testClick() {
        console.log('testClick ...')
      },
      testChildClick() {
        console.log('testChildClick ...')
      }
    }
  }
</script>

<!-- children 组件 -->
<div class="children">
  children - {{name}}
  <grandson v-bind="$attrs" :name="name" v-on="$listeners"></grandson>
</div>

<script>
  export default {
    created() {
      console.log('children -----------------')
      this.$emit('testClick')
      console.log('children -----------------')
    }
  }
</script>

<!-- grandson 组件 -->
<div class="grandson">
  <div>name:{{name}}</div>
  <div>age:{{age}}</div>
  <div>sex:{{$attrs['sex']}}</div>
</div>

<script>
  export default {
    created() {
      console.log('grandson -----------------')
      this.$emit('testClick')
      this.$emit('testChildClick')
      console.log('grandson -----------------')
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

打印结果:

children -----------------
testClick ...
children -----------------

grandson -----------------
testClick ...
testChildClick ...
grandson -----------------
1
2
3
4
5
6
7
8

注意的点

  1. children 组件并没有定义任何方法,而 grandson 却 2 个方法都可以调用

  2. 如果 children 定义了方法,那么 chilren 的会执行parent 也会执行

小结

$listeners 就是用于传递事件的,如果传递的过程中有同名的事件,那么 2 个事件都会被执行


# 题外拓展 inheritAttrs

官方介绍inheritAttrs (opens new window)

大概的作用: 默认为 true 的时候,会在子组件的节点上添加对应的参数名,这可能会影响我们自己定义参数,所以提供了 inheritAttrs 属性 ,让我们自己操作 dom 的时候可以自定义标签的属性。

  • 默认值 true,继承所有的父组件属性(除 props 的特定绑定)作为普通的 HTML 特性应用在子组件的根元素上

  • 如果你不希望组件的根元素继承特性设置 inheritAttrs: false,但是 class 属性会继承

  • 简单的说

    1. inheritAttrs:true 继承除 props 之外的所有属性
    2. inheritAttrs:false继承 class 属性)
    3. 注意这里是继承,而不是 props 传值,继承和传值是有区别的

还是一开始 $attrs的 demo。默认情况下:标签上的属性都来自props定义的。

  • childred 组件的 props:['age',sex']
  • grandson 组件的 props:['sex']
  • 组件内的 inheritAttrs 默认为 true

字不如图。

操作 1:children 组件中添加 inheritAttrsfalse

可以看到 children 标签的属性被隐藏了。可是 name 还是可以输出出来。class 不受影响

这时候如果有需求需要在 children 标签上放一些自定义的属性(比如想自定义 name 和 age),也不会和 vue 的机制冲突了

# 最后附上 demo

parent 组件
<template>
  <div class="parent">
    parent
    <children
      :name="name"
      :age="age"
      :sex="sex"
      @testClick="testClick"
      @testChildClick="testChildClick"
    ></children>
  </div>
</template>

<script>
  import children from './children'
  export default {
    name: 'parent',
    data() {
      return {
        name: 'Jioho',
        age: 18,
        sex: '男'
      }
    },
    components: {
      children: children
    },
    methods: {
      testClick() {
        console.log('testClick ...')
      },
      testChildClick() {
        console.log('testChildClick ...')
      }
    }
  }
</script>

<style lang="scss" scoped>
  .parent {
    width: 400px;
    height: 400px;
    background-color: green;
    text-align: center;
    color: #ffffff;
    font-size: 18px;
  }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
children 组件
<template>
  <div class="children">
    children - {{name}}
    <grandson v-bind="$attrs" :name="name" v-on="$listeners" @testChildClick="testChildClick"></grandson>
  </div>
</template>

<script>
  import grandson from './grandson'

  export default {
    name: 'children',
    props: ['name'],
    inheritAttrs: false,
    components: {
      grandson
    },
    created() {
      console.log('children -----------------')
      this.$emit('testClick')
      console.log('children -----------------')
    },
    methods: {
      testChildClick() {
        console.log('子组件调用了')
      }
    }
  }
</script>

<style lang="scss" scoped>
  .children {
    width: 300px;
    height: 300px;
    background-color: rgb(138, 194, 138);
    text-align: center;
    color: #ffffff;
    font-size: 18px;
  }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
grandson 组件
<template>
  <div class="grandson">
    <div>name:{{name}}</div>
    <div>age:{{age}}</div>
    <div>sex:{{$attrs['sex']}}</div>
  </div>
</template>

<script>
  export default {
    name: 'grandson',
    props: ['name', 'age'],
    created() {
      console.log('grandson -----------------')
      this.$emit('testClick')
      this.$emit('testChildClick')
      console.log('grandson -----------------')
    }
  }
</script>

<style lang="scss" scoped>
  .grandson {
    width: 200px;
    height: 200px;
    background-color: blue;
    color: #ffffff;
    font-size: 18px;
  }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 方式 4:$parent / $children 与 ref

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
  • $parent / $children:访问父 / 子实例

这两种方法的弊端是,无法在跨级或兄弟间通信。

拿到实例对象后,就可以进行值的获取,或者修改了,这里就不做展开

# 方式 5:provide/inject

Vue2.2.0 新增 API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。

看个栗子:

// 父级组件提供 'foo'
export default {
  data() {
    return {
      foo: 'bar'
    }
  },
  provide() {
    return {
      foo: this.foo
    }
  }
}

// 子组件注入 'foo'
export default {
  inject: ['foo']
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

从这几个问题,了解下 provide/inject

# provide 什么时候可以为对象,什么时候为函数

provide 如果是直接传值(无须从当前页面获取 this 里面的值的话)可以为对象,

如果需要用到当前页面的变量,那就需要函数,来获取 this 对象

// 示范1 provide为对象的时候
export default {
  provide: {
    foo: 'bar' // 这时候是直接赋值,无须this
  }
}

// 示范2 provide的值依赖于当前的this
export default {
  data() {
    return {
      foo: 'bar'
    }
  },
  provide() {
    return {
      foo: this.foo // 这时候依赖于this,就需要为函数,具体的要理解下原型和原型链。和data为啥不能是对象一个道理
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# provideinject什么时候是单向数据,什么时候可以双向绑定?

在问题 1 中的代码,数据都是单向数据流,如果在子组件中改变了 foo 。他们并不会影响父组件的值。同理,父组件的 foo 修改了,子组件在 inject 接收到的值也不会变化。这是 vue 故意这样设计的

那有没有双向数据绑定的栗子?有的:上述的情况都是因为传进去的值都是非响应式的值(相当于只是传递了一个字符串)。只要我们传入的值是一个响应式对象。那他们就可以实现双向数据绑定












 






 


 
 



// 父组件传入 foo 对象,注意是对象类型而不是单纯的字符串
export default {
  data() {
    return {
      foo: {
        value: 'bar'
      }
    }
  },
  provide() {
    return {
      foo: this.foo // 传入一个响应式对象,因为不是字符串,所以会被监听到
    }
  }
}

// 子组件接收该对象
export default {
  inject: ['foo'],
  create() {
    console.log(this.foo) // {value:'bar'}
    // 成功,并且父组件的 thisi.foo.value 也被修改为了new bar
    this.foo.value = 'new bar'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# provide/inject 好处在哪里和缺点

好处

  • provide/inject 相当于上面说的 $attrs。可以说更加优雅了一些。甚至无须一层层传递 $attrs

  • provide/inject 也可以做全局状态管理(vuex)但是不建议这么做。上面问题 2 也说到了,只要传入的是响应式对象,那他们都可以双向数据绑定。

    • 建议:如果用 provide/inject 充当全局状态管理的话,只推荐用一些不会变化(一次性数据),可是需要贯穿全局的变量去传递。
    • 总结了两条条使用 provide/inject 做全局状态管理的原则:
      1. 多人协作时,做好作用域隔离
      2. 尽量使用一次性数据作为全局状态
  • 利用 provide/inject 的特性来编写组件库非常有用。可以实现不同组件间的嵌套,而不需要一层层的数据传递。(可以参考下 elementUI 的表单和按钮设计)

缺点

就是刚才说到的全局状态管理的问题。为什么 provide/inject 不能代替 vuex 成为全局数据管理的首选?

Vuex 和 provide/inject 最大的区别在于,Vuex 中的全局状态的每次修改是可以追踪回溯的,而 provide/inject 中变量的修改是无法控制的,换句话说,你不知道是哪个组件修改了这个全局状态

试想,如果有多个后代组件同时依赖于一个祖先组件提供的状态,那么只要有一个组件修改了该状态,那么所有组件都会受到影响。这一方面增加了耦合度,另一方面,使得数据变化不可控。如果在多人协作开发中,这将成为一个噩梦

# 总结

组件之间传值通信的方式非常非常多,每种方案各有优缺点,并不能一套方案打天下,主要还是要结合自身的业务逻辑和复杂程度,选择最优的方法

Last Updated: 5/9/2021, 10:48:13 PM