组件之间通信, 主要存在于下面三种情况:
- 父子组件之间(Index-A、Index-B...)
- 兄弟组件之间(A-B)
- 隔代关系组件之间(Index-C, Index-D)
那么具体他们之间如何进行通信, 我们一一解答
首先先看下文件的目录结构
接下来看看具体的通信方式
1. props & $emit
1.1 父传子 props
现在我们要从Index页面给A页面传递一个数组list
// index.vue
<template>
  <div>
    <A :list="list" />
  </div>
</template>
<script>
import A from "./components/A";
export default {
  name: "Index",
  components: {
   A
  },
  data() {
   return {
     list: ["html", "css", "js"],
   };
  },
},
</script>
// A.vue
<template>
  <div>
    <h2>Page A</h2>
    <ul v-for="(item, index) in list" :key="index">
      <li>{{ item }}</li>
    </ul>
  </div>
</template>
<script>
export default {
  name: "A",
  props: ["list"],
};
</script>
效果图如下, 此时list数组以自上而下的一种方式从Index页面传递给A组件, 且props只可以从上一级想下一级传输, 即所谓的单向数据流
1.2 子传父 $emit
那A组件想给Index页面传送数据应该如何操作? 关于子组件给父组件传值, 一般都是通过一个事件搭配$emit进行传输
// A.vue
<template>
  <div>
    <h2>Page A</h2>
    <ul v-for="(item, index) in list" :key="index">
       <!-- 定义事件 -->
      <li @click="onItemClick(item)">{{ item }}</li>
    </ul>
  </div>
</template>
<script>
export default {
  name: "A",
  props: ["list"],
  methods: {
    onItemClick(item) {
      this.$emit("on-item-click", `'${item}' from page A`);
    },
  },
};
</script>
父组件监听子组件上的事件名on-item-click
// index.vue
<template>
  <div>
    <!-- 监听 -->
    <A :list="list" @on-item-click="handleItemClick" />
  </div>
</template>
<script>
import A from "./components/A";
export default {
  name: "Index",
  components: {
    A,
  },
  data() {
    return {
      list: ["html", "css", "js"],
    };
  },
  methods: {
    handleItemClick(value) {
      console.log(`In page Index get ${value}`);
    },
  },
};
</script>
此时, 点击li元素的时候, 我们就可以在Index页面获取到A页面传递过来的值
2. $children & $parent
$parent的类型为: Vue instance
$children的类型为: Array<Vue instance>, 需要注意$children并不保证顺序,也不是响应式的
因为这两个API拿到的都是vue实例, 所以可以访问父组件或子组件身上变量, 方法等; 使用方式如下:
在A页面中定义了一个变量msg, 通过$parent获取父组件中的数据
// A.vue
<template>
  <div>
    <h2>Page A</h2>
    <p>{{ msg }}</p>
    <ul v-for="(item, index) in $parent.list" :key="index">
      <li @click="onItemClick(item)">{{ item }}</li>
    </ul>
    <hr />
  </div>
</template>
<script>
export default {
  name: "A",
  data() {
    return {};
  },
  // 获取Index中的list
  mounted() {
    console.log('A mounted', this.$parent.list);
  },
};
</script>
现在我们在父组件中通过$children获取到A页面中的msg并修改
// Index.vue
<template>
  <div>
    <A :list="list" @on-item-click="handleItemClick" />
    <button @click="handleChange">change A's msg</button>
  </div>
</template>
<script>
import A from "./components/A";
export default {
  name: "Index",
  components: {
    A,
  },
  data() {
    return {
      list: ["html", "css", "js"],
    };
  },
  methods: {
    handleItemClick(value) {
      console.log(`In page Index get ${value}`);
    },
    handleChange() {
      console.log((this.$children[0].msg = "A's data is changed"));
    },
  },
};
</script>
3. ref
ref如果用在普通的DOM元素上, 引用指向的就是DOM元素本身; 如果使用在组件上, 引用指向的就是该组件的实例
// index.vue
<template>
  <div>
    <A ref="componentA" :list="list" @on-item-click="handleItemClick" />
    <p ref="p">p标签</p>
  </div>
</template>
<script>
import A from "./components/A";
export default {
  name: "Index",
  components: {
    A,
  },
  mounted() {
    console.log("componentA ref", this.$refs.componentA);
    console.log("p ref", this.$refs.p);
  },
};
</script>
下面是打印的结果
当
ref和v-for一起使用的时候,$refs得到的是一个数组
4. provide & inject
- 
provide:Object | () => Object
- 
inject:Array<string> | { [key: string]: string | Symbol | Object }
这对选项需要一起使用, 允许一个祖先组件向所有后代注入一个依赖, 不论层级有多深, 并在其上下游关系成立的时间里始终生效
我们现在来看一下具体的例子: Index -> A -> C
先在Index中提供数据
// index.vue
export default {
  name: "Index",
  components: {
    A,
  },
  provide: {
    name: "name from Index",
  },
  data() {
    return {
      list: ["html", "css", "js"],
    };
  },
  methods: {
    handleItemClick(value) {
      console.log(`In page Index get ${value}`);
    },
    handleChange() {
      console.log((this.$children[0].msg = "A's data is changed"));
    },
  },
};
然后在C组件中通过inject获取值
// C.vue
export default {
  name: "C",
  inject: {
    value: "name",
    data: {
      from: "data1",
      default: "1",
    },
  },
  mounted() {
    // Index 中并没有在 provide 中提供 data1 变量, 所以C组件会取默认值
    console.log("C", this.value, this.data); // C  name from Index  1
  },
};
5. EventBus($emit, $on, $off)
这种方式是通过创建一个空的Vue实例作为事件总线, 用它来触发事件, 监听事件, 解除事件, 从而实现任何组件之间的通信, 然后当项目逐渐扩大, 这种通信方式还是不建议选择, 难以维护
使用EventBus来通信主要有以下几个步骤
5.1 创建一个事件总线并将其导出
// event-bus.js
import Vue from "vue";
export const EventBus = new Vue();
5.2 发送一个事件
主要有Index、A、B、C几个页面
// index.vue
<template>
  <div>
    <A />
    <B />
  </div>
</template>
// A.vue
<template>
  <div>
    <h2 @click="handleClick">Page A</h2>
    <C />
  </div>
</template>
<script>
import C from "./C";
// 引入
import { EventBus } from "../event-bus.js";
export default {
  name: "A",
  components: {
    C,
  },
  methods: {
    // 触发事件
    handleClick() {
      EventBus.$emit("transfer-by-event-bus", "eventBus data");
    },
  },
};
</script>
5.3 监听事件, 接收数据
// B.vue
<template>
  <div>
    <h2>Page B</h2>
  </div>
</template>
<script>
import { EventBus } from "../event-bus";
export default {
  name: "B",
  mounted() {
    // 监听事件
    EventBus.$on("transfer-by-event-bus", value => console.log("B", value)); // B eventBus data
  },
};
</script>
// C.vue
<template>
  <h5>C</h5>
</template>
<script>
import { EventBus } from "../event-bus";
export default {
  name: "C",
  mounted() {
    EventBus.$on("transfer-by-event-bus", value => console.log("C", value)); // C eventBus data
  },
};
</script>
5.4 移除事件
import { eventBus } from 'event-bus.js'
EventBus.$off('transfer-by-event-bus', {})
6. $attrs & $listeners
6.1 $attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (
class和style除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class和style除外),并且可以通过v-bind="$attrs"传入内部组件——在创建高级别的组件时非常有用
我们先通过Index页面向B组件传递name, sex, age三个值
// index.vue
<template>
  <div>
    <B :name="name" :sex="sex" :age="age" @on-click="handleClick" />
  </div>
</template>
<script>
import B from "./components/B";
export default {
  name: "Index",
  components: {
    B,
  },
  data() {
    return {
      name: "Lily",
      sex: "female",
      age: "20",
    };
  },
};
</script>
然后再B组件中我们打印一下$attrs, 看看能获取什么值? 可以看见$attrs返回的是一个对象, 且键值对就是我们在Index页面传给B组件的值
<template>
  <div>
    <h2>Page B</h2>
  </div>
</template>
<script>
export default {
  name: "B",
  mounted() {
    console.log(this.$attrs); // {name: "Lily", sex: "female", age: "20"}
  },
如果此时我们在B组件的props中接收一个变量, 看看有何变化? 打印出来的值不包含props获取的name字段
<template>
  <div>
    <h2>Page B</h2>
  </div>
</template>
<script>
export default {
  name: "B",
  props: {
    name: String,
  },
  mounted() {
    console.log(this.$attrs); // {sex: "female", age: "20"}
  },
现在需要把这几个变量再继续传给B组件的子组件D, 我们直接通过v-bind="$attrs"即可
// B.vue
<template>
  <div>
    <h2>Page B</h2>
    <D v-bind="$attrs" />
  </div>
</template>
<script>
import D from "./D";
export default {
  name: "B",
  components: {
    D,
  },
  props: {
    name: String,
  },
  mounted() {
    console.log(this.$attrs); // {sex: "female", age: "20"}
  },
D组件同样获取到了这几个值
// D.vue
<template>
  <h5>D</h5>
</template>
<script>
export default {
  name: "D",
  mounted() {
    console.log(this.$attrs); // {sex: "female", age: "20"}
  },
};
</script>
6.2 $listeners
包含了父作用域中的 (不含
.native修饰器的)v-on事件监听器。它可以通过v-on="$listeners"传入内部组件——在创建更高层次的组件时非常有用
当子组件需要调用父组件中的方法, 我们就可以通过$listeners来调用, 但前提是方法名必须在父组件中被定义
我们实操一下, 先在Index页面的分别定义两个事件
<template>
  <div>
    <B
      @click1="handeClick1"
      @click2="handeClick2"
    />
  </div>
</template>
<script>
import B from "./components/B";
export default {
  name: "Index",
  components: {
    B,
  },
  data() {
    return {};
  },
  methods: {
    handeClick1() {
      console.log("1");
    },
    handeClick2() {
      console.log("2");
    },
  },
};
</script>
子组件B调用方法如下, 现在我们分别点击one, two两个文本就可以分别打印出1, 2
// B.vue
<template>
  <div>
    <h2 @click="$listeners.click1">one</h2>
    <h2 @click="$listeners.click2">two</h2>
  </div>
</template>
<script>
export default {
  name: "B",
  data() {
    return {};
  },
};
</script>
如果再向下传递给B的子组件D也是没有问题的, 我们只需要将$listeners传下就可以了
// B.vue
<template>
  <div>
    <h2 @click="$listeners.click1">one</h2>
    <h2 @click="$listeners.click2">two</h2>
    <D v-on="$listeners" />
  </div>
</template>
D组件也像B组件那样调用事件即可
// D.vue
<template>
  <h5 @click="$listeners.click1">D</h5>
</template>
7. Vuex
Vuex想必大家应该很熟悉, 它是一个专为Vue.js应用程序开发的状态管理模式, 它让开发者能够聚焦于数据的更新而不是数据的传递
Vuex主要有以下几个模块
- 
state: 用于数据的存储, 是store中的唯一数据源
- 
getters: 同vue中的计算属性, 基于state数据进行二次包装来获取符合条件的数据
- 
mutations: 处理同步事件, 是唯一更改 store 中状态的方法
- 
actions: 可包含异步操作, 用于提交mutation, 不可直接变更状态
- 
modules: 类似于命名空间, 用于项目中将各个模块的状态分开定义和操作, 便于维护
8. 总结
我们现在就开头说的三种场景再进行一次总结, 但是我们还是需要根据当下的场景选择合适的通信方式~
- 父子组件之间: props & $emit,$chilren & $parent,ref,EventBus,provide & inject,Vuex
- 兄弟组件之间: Vuex,EventBus
- 隔代关系组件之间: EventBus,$attrs & $listeners,provide & inject,Vuex