Auc的个人博客

vuePress-theme-reco Auc    2020 - 2021
Auc的个人博客 Auc的个人博客

选择一个主题吧~

  • 暗黑
  • 自动
  • 明亮
主页
分类
  • JavaScript
  • Vue
  • 数据结构与算法
  • 文档
  • 面试题
  • 笔记
标签
笔记
  • CSS
  • ES6
  • JavaScript
  • Vue
  • C语言
文档
  • Vueのapi
  • Vue Router
  • axios
  • Vue CLI
面试
  • JS
  • Vue
  • 基础
时间线
联系我
  • GitHub (opens new window)
author-avatar

Auc

62

文章

11

标签

主页
分类
  • JavaScript
  • Vue
  • 数据结构与算法
  • 文档
  • 面试题
  • 笔记
标签
笔记
  • CSS
  • ES6
  • JavaScript
  • Vue
  • C语言
文档
  • Vueのapi
  • Vue Router
  • axios
  • Vue CLI
面试
  • JS
  • Vue
  • 基础
时间线
联系我
  • GitHub (opens new window)
  • Vue

    • Vue 无法检测的数组改动问题
    • Vue 组件之间的通信方式
    • 造轮子 - 双向数据绑定

Vue 组件之间的通信方式

vuePress-theme-reco Auc    2020 - 2021

Vue 组件之间的通信方式

Auc 2020-07-07 Vue

# Vue 组件之间的通信方式

# props 和 $emit

适用于父子组件之间互相通信

详见 Vue 组件基础

# $emit 与 $on (事件总线)

适用于父子、隔代、兄弟组件通信

# 方法

  1. 在全局注册一个空的 Vue 实例,比如名为: EventBus。
  2. 在需要通信的组件中调用该空实例的 EventBus.$emit 触发事件,EventBus.$on 监听触发的事件。

# 示例

  1. 功能

功能是完成兄弟组件之间的通信:A => C, B => C

  1. 效果展示

  2. 全局注册空的 Vue 实例,并创建根组件。

// 在全局注册一个空的 Vue 实例
const EventBus = new Vue();
// 根组件
const vm = new Vue({
  el: '#test',
  components: {
    'test-a': A,
    'test-b': B,
    'test-c': C,
  }
});
1
2
3
4
5
6
7
8
9
10
11
  1. 配置上面局部注册的子组件

HTML 部分

<div id="test">
  <test-a></test-a>
  <test-b></test-b>
  <test-c></test-c>
</div>
1
2
3
4
5

JS 部分

// 组件 A
const A = {
  template:`
    <div>
      <p>Compontent A</p>
      <button @click="sendMsg">A To C</button>
    </div>
  `,
  data () {
    return {
      valueA: 'message from A'
    };
  },
  methods: {
    sendMsg () {
      // 在实例 EventBus 上利用 $emit 注册自定义事件,名为 aSend
      EventBus.$emit('aSend', this.valueA);
    }
  }
};
// 组件 B
const B = {
  template:`
    <div>
      <p>Compontent B</p>
      <button @click="sendMsg">B To C</button>
    </div>
  `,
  data () {
    return {
      valueB: 'message from B'
    };
  },
  methods: {
    sendMsg () {
      // 在实例 EventBus 上利用 $emit 注册自定义事件,名为 bSend
      EventBus.$emit('bSend', this.valueB);
    }
  }
};
// 组件 C
const C = {
  template:`
    <div>
      <p>Compontent C</p>
      <div>From other component: {{ valueC }}</div>
    </div>
  `,
  data () {
    return {
      valueC: ''
    };
  },
  // 使用空 Vue 实例 EventBus 中的 $on() 监听当前实例(即:EventBus 实例)上的自定义事件
  // 也就是前面利用 $emit 注册的事件:aSend 和 bSend
  mounted () {
    EventBus.$on('aSend', (value) => {
      this.valueC = value
    });
    EventBus.$on('bSend', (value) => {
      this.valueC = value
    });
  }
};
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
59
60
61
62
63
64

从上例很容易看出

利用事件总线这个方案很巧妙,通过全局注册的空 Vue 实例,各个需要通信的组件中,发送方调用空实例的$emit,接收方调用空实例的 $on

即:在这个空 Vue 实例身上

  • 利用 $emit() 注册自定义事件,并利用其第二个参数承载需要发送的数据。
  • 利用 $on() 监听注册的自定义事件,并利用其第二个参数(callback)处理 $emit() 第二个参数的数据。

# $attrs 与 $listeners

Vue v2.4 中新增的两个实例 property,可用于隔代组件之间的数据传递。

# $attrs

1. 功能描述 - 用于向下隔代通信 - 首先在父组件上绑定的一个属性,欲将此数据传递给子组件,但是子组件并没有按套路 props 接收,那就会自动的被子组件的 $attrs 实例属性接收。 - 再通过父传子或者直接通过 v-bind="$attrs" 来将此数据传递给该子组件的下一代组件,数据流从第一代组件 => 第三代组件,俗称: "隔代通信"。

2. $attrs 是一个对象

$attrs 为一个对象里面以键值对的形式存着父组件传递过来并且没被 props 接收的数据。

3. 示例

欲从 Grandpa 隔代传递数据到 Grandson

数据流图

第一代组件

// 根组件
const vm = new Vue({
  el: '#test',
  // 这里欲将数据传递到隔代组件,要先经过第二代组件
  template: `
    <div>
      <p>Grandpa</p>
      <button @click="sendMsg">Grandpa To Grandson</button>
      <son :fromGrandpa="value"></son>
    </div>
  `,
  data: {
    value: {}
  },
  methods: {
    sendMsg () {
      this.value = 'message from grandpa';
    }
  },
  components: {
    son: Son
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第二代组件

介绍

这里利用了 Object.keys() API,详见:MDN: Object.keys() (opens new window)。

  1. 功能:将一个对象的所有可枚举键名存到一个数组中。(注意深度为一,即属性为引用类型内部无法作用到)
  2. 参数:欲转换的对象。
  3. 返回值:一个表示给定对象的所有可枚举属性的字符串数组。
// 第二代组件
const Son = {
  template:`
    <div>
      <p>Son</p>
      <p>Son' $attrs = {{ this.$attrs[Object.keys(this.$attrs)[0]] }}</p>
      <grandson :fromGrandpa="$attrs"></grandson>
    </div>
  `,
  components: {
    grandson: Grandson
  },
  updated () {
    console.log(this.$attrs);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注意

  1. 在这里使用 Object.keys() 是因为 $attrs 为一个对象形式,这个对象内部存储着从父组件传递过来没被 props 接收的数据,数据是键值对的形式。而我们只需要这个对象中的其中一个键值。
  2. 第二代 ==> 第三代
    • 这里利用父传子将 $attrs 传递给第三代组件,第三代组件中利用 props 接收此数据。
    • 也可以直接通过 v-bind="$attrs" 来将此数据传递给下一代组件,这样的话 props 就接收不到,所以在第三代组件上存到了 $attr 里面,就得从 $attrs 里取了。

第三代组件

// 第三代组件
const Grandson = {
  template:`
    <div>
      <p>Grandson</p>
      <div>Grandson's $attrs: {{ fromGrandpa[Object.keys(this.fromGrandpa)[0]] }}</div>
      <div>From Grandpa: {{ fromGrandpa[Object.keys(this.fromGrandpa)[0]] }}</div>
    </div>
  `,
  props: ['fromGrandpa'],
};
1
2
3
4
5
6
7
8
9
10
11

💯读到这里有读者可能会说,这种方式不就是从第一代组件传数据到下一代,再从下一代在向下传一代,经历过两次父传子吗?是的,的确是这样的,只不过不同点是使用了 $attrs 来自动存储没被 props 接收的数据。

# $listeners

子组件通过 $emit() 触发的事件,都会记录在父组件的 $listeners 中,在父组件中就可以通过 v-on=$listeners 的方式继续向上一层组件传递。

1. $listeners 是个对象

$listeners 为一个对象里面存着子组件 $emit() 过来方法。

2. 示例

使用方法与 $attrs 类似

数据流图

第三代组件

联想子组件到父组件通信,利用 $emit。

// 第三代组件
const Grandson = {
  template:`
    <div>
      <p>Grandson</p>
      <button @click="$emit('fromGrandson', 'msg from grandson')">To Grandpa</button>
    </div>
  `
};
1
2
3
4
5
6
7
8
9

第二代组件

利用 v-on="$listeners" 记录

// 第二代组件
const Son = {
  template:`
    <div>
      <p>Son</p>
      <grandson v-on="$listeners"></grandson>
    </div>
  `,
  components: {
    grandson: Grandson
  },
  updated () {
    // 可知 $listeners 为一个对象里面存着子组件 $emit() 过来方法
    console.log(this.$listeners);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第一代组件

数据到达目标组件,这里就用传统的子传父,利用自定义事件接收传递过来的这个事件 fromGrandson

// 根组件
const vm = new Vue({
  el: '#test',
  template: `
    <div>
      <p>Grandpa</p>
      <p>Message: {{ value }}</p>
      <son @fromGrandson="changeValue"></son>
    </div>
  `,
  data: {
    value: ''
  },
  methods: {
    changeValue (msg) {
      this.value = msg;
    }
  },
  components: {
    son: Son
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# $parents/$children 与 ref

# $parents/$children

这个方法就很爽了,首先观察一下我们的 vm 实例。

  • $parents 保存有指向该组件实例的父组件实例的指针。 { Vue instance }
  • $children 保存有指向该组件实例的子组件实例的指针。 { Array }

注意

  1. Vue instance 字面意思为 Vue 实例,我是按 Object 理解的。
  2. 两个属性都是明确标注只读的,
  3. 【TODO】是不是指针存疑,按指针理解行得通。

这就省事了,需要取值的时候利用 $parents 召唤父组件,利用 $children 召唤子组件,比较适合用于父子组件之间进行通信,当然使用套娃手段层层召唤进行通信也是可以,当然我们处理隔代、任意组件之间通信有更好的方法。

注意

这里用的是取值,指的是召唤 $parents/$children 并拿它内部的值来用,这实际意义上并不算"传值",说"拿值"比较贴切。

看一个例子

数据流图

// 子组件 A
const SonA = {
  template:`
    <div>
      <p>Son-A</p>
      <button @click="getFaMsg">get fater's msg</button>
      <p>show the msg from Fa: {{ fromFaVal }}</p>
    </div>
  `,
  data () {
    return {
      sonVal: 'msg from Son-A',
      fromFaVal: ''
    };
  },
  methods: {
    getFaMsg () {
    // $parent { Object } 指向该组件实例的父组件实例
    // 直接召唤父组件来调它内部的数据
      this.fromFaVal = this.$parent.value;
    }
  },
  mounted () {
    console.log(this.$parent);
  }
};
// 子组件 B
const SonB = {
  template: `
    <div>
      <p>Son-B</p>
      <p>Son-B' value: {{ sonVal }}</p>
      <button @click="getFaMsg">get fater's msg</button>
      <p>show the msg from Fa: {{ fromFaVal }}</p>
    </div>
  `,
  data() {
    return {
      sonVal: 'msg from Son-B',
      fromFaVal: ''
    };
  },
  methods: {
    getFaMsg() {
      this.fromFaVal = this.$parent.value;
    }
  },
  mounted () {
    // $parent { Object } 指向该组件实例的父组件实例
    console.log(this.$parent);
  }
};
// 根组件
const vm = new Vue({
  el: '#test',
  template: `
    <div>
      <p>Father</p>
      <button @click="getSonValA">get msg from Son-A</button>
      <button @click="getSonValB">get msg from Son-B</button>
      <p>the value from son: {{ fromSonVal }}</p>
      <son-a></son-a>
      <son-b></son-b>
    </div>
  `,
  data: {
    value: 'this is a msg from father',
    fromSonVal: ''
  },
  methods: {
    getSonValA () {
      this.fromSonVal = this.$children[0].sonVal;
    },
    getSonValB() {
      this.fromSonVal = this.$children[1].sonVal;
    }
  },
  components: {
    'son-a': SonA,
    'son-b': SonB
  },
  mounted () {
    // $children { Array } 存储该组件实例对象的子组件实例对象
    // 存储顺序为子组件 mounted 的顺序
    console.log(this.$children);
  }
});

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

# ref 与 $refs

这个跟上面类似,都是数据直接从指定组件中拿数据来用。这个适用于父组件从下级子组件拿数据(当然,套娃战术层层获取也可以)

  • ref 给子组件注册,相当于给了该子组件一个 id,方便通过这个 id 来找到该子组件从其中拿数据。
  • $refs 一个对象,持有注册过 ref 的所有 DOM 元素和组件实例, 键名为注册 ref 时的值

看一个例子

数据流图

// 子组件 A
const SonA = {
  template:`
    <div>
      <p>Son-A</p>
      <p>{{ sonVal }}</p>
    </div>
  `,
  data () {
    return {
      sonVal: 'msg from Son-A',
    };
  }
};
// 子组件 B
const SonB = {
  template: `
    <div>
      <p>Son-B</p>
      <p>{{ sonVal }}</p>
    </div>
  `,
  data() {
    return {
      sonVal: 'msg from Son-B',
    };
  }
};
// 根组件
const vm = new Vue({
  el: '#test',
  // 给两个不同的组件注册不同的 ref,用于区分。
  template: `
    <div>
      <p>Father</p>
      <button @click="getSonValA">get msg from Son-A</button>
      <button @click="getSonValB">get msg from Son-B</button>
      <p>the value from son: {{ fromSonVal }}</p>
      <son-a ref="sonA"></son-a>
      <son-b ref="sonB"></son-b>
    </div>
  `,
  data: {
    fromSonVal: ''
  },
  methods: {
    // 通过 $refs 找到注册有不同 ref 的组件,并从中拿值。
    getSonValA () {
      this.fromSonVal = this.$refs.sonA.sonVal;
    },
    getSonValB() {
      this.fromSonVal = this.$refs.sonB.sonVal;
    }
  },
  components: {
    'son-a': SonA,
    'son-b': SonB
  },
  mounted () {
    console.log(this.$refs);
  }
});
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
59
60
61
62

# provide / inject

这两个 API 很强

  1. provide
    • 功能:在父组件上使用,内部为可以被下级所有组件共享的数据
    • 类型:为一个对象或返回一个对象的函数 { Object | () => Object }。
  2. inject
    • 功能:在下级组件中使用,用于接受上级组件中 provide 里面共享的数据。
    • 类型:一个字符串数组或者一个对象。

注意

  1. provide 和 inject 主要在开发高阶插件/组件库时使用。比如:element-ui 的 tabs 和 select ,但并不推荐用于普通应用程序代码中。
  2. provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

看一个例子

数据流图

// 子组件 A
const SonA = {
  template:`
    <div>
      <p>Son-A</p>
      <button @click="getFa">get msg from Fa</button>
      <p>show: {{ msgFromFa }}</p>
    </div>
  `,
  data () {
    return {
      msgFromFa: ''          
    };
  },
  methods: {
    getFa () {
      this.msgFromFa = this.faMsg;
    }
  },
  // 子组件利用 inject 来接收
  inject: ['faMsg'],
  created () {
    // 虽然给了个数组,但是是个对象
    console.log(this.$options.inject);
  }
};
// 根组件
const vm = new Vue({
  el: '#test',
  // 给两个不同的组件注册不同的 ref,用于区分。
  template: `
    <div>
      <p>Father</p>
      <p>Father's msg: {{ msg }}</p>
      <son-a></son-a>
    </div>
  `,
  data: {
    msg: 'this is a msg from father'
  },
  // 该对象可被其下级注册有 inject 的所有子孙组件共享
  provide: {
    faMsg: 'this is a msg from father'
  },
  components: {
    'son-a': SonA
  },
  created () {
    // 虽然给了个对象,但是打印一下 provide 是个函数
    console.log(this.$options.provide);
  }
});
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

下面是两者的打印,感兴趣的童鞋可以研究一下。

# 通过 vuex

这个不多讲了,用起来很舒服,项目中频繁用到。

# 参考

  1. Vue 官方文档 (opens new window)
  2. 掘金:$attrs 与 $listeners 的介绍及使用 (opens new window)
  3. 简书:vue组件间通信六种方式 (opens new window)