Vue通信的几种方式

组件之间的关系

组件之间通信, 主要存在于下面三种情况:

  • 父子组件之间(Index-A、Index-B...)
  • 兄弟组件之间(A-B)
  • 隔代关系组件之间(Index-C, Index-D)

那么具体他们之间如何进行通信, 我们一一解答

首先先看下文件的目录结构

image.png

接下来看看具体的通信方式

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只可以从上一级想下一级传输, 即所谓的单向数据流

image.png
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页面传递过来的值

image.png

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>
image.png

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>

下面是打印的结果

image.png

refv-for 一起使用的时候, $refs 得到的是一个数组

4. provide & inject

  • provideObject | () => Object
  • injectArray<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 绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容