Skip to content

组件数据共享

组件之间的关系大体上分为:父子关系、兄弟关系、后代关系这 3 大类。如何巧妙灵活的实现不同关系的组件之间的数据共享,是我们接下来要学习的内容。

Props

Props 适用于父组件向子组件共享数据,如果要实现多层嵌套组件之间的数据共享,则需要逐层向下传入 Props。

dark

事件

自定义事件适用于子组件向父组件共享数据。

在子组件中触发自定义事件,在父组件中使用 v-on 绑定事件监听器,通过形参获取到子组件向上传出的数据。

依赖注入

依赖注入适用于父组件向后代组件共享数据。

在父组件中使用 Provide 向后代提供数据,在后代组件中使用 Inject 获取注入的数据(也是单向的:自上而下的数据流动)。

dark

Provide

provide() 函数是数据的提供者,用于向后代组件提供数据。语法格式如下:

vue
<script setup>
import { provide } from 'vue'

// 第一个参数是数据的名称,可以是字符串或者 Symbol
// 第二个参数是要提供给后代组件的数据,可以是任意类型的数据
provide('数据名', 要提供的数据)
</script>

例如,在 App 组件中使用 provide 向下提供数据:

vue
<script setup>
import LevelA from './components2/LevelA.vue'
import { provide, reactive, computed } from 'vue'

// 响应式的数据对象
const rgb = reactive({
  r: 0,
  g: 0,
  b: 0
})
// 计算属性
const rgbColor = computed(() => `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`)
// 向下提供数据
provide('color', rgbColor)
</script>

<template>
  <h1>这是 App 组件 --- {{ rgbColor }}</h1>
  <!-- 双向数据绑定 -->
  <input type="number" v-model="rgb.r" />
  <input type="number" v-model="rgb.g" />
  <input type="number" v-model="rgb.b" />
  <hr />
  <LevelA />
</template>

🚨 注意 🚨

一个组件可以多次使用 Provide 向下提供不同的数据。

Inject

在后代组件中,可以通过 inject() 注入祖先提供的数据。语法格式如下:

vue
<script setup>
import { inject } from 'vue'

const message = inject('数据名')
</script>

例如,在 LevelC 组件中使用 inject 来注入数据:

vue
<script setup>
import { inject } from 'vue'
// 注入祖先向下共享的数据
const color = inject('color')
</script>

<template>
  <h6>这是 LevelC 组件 --- {{ color }}</h6>
  <div class="box"></div>
</template>

<style scoped>
.box {
  width: 200px;
  height: 200px;
  /* 绑定背景颜色 */
  background-color: v-bind('color');
}
</style>

🚨 注意 🚨

一个组件可以多次调用 Inject 注入祖先提供的不同数据。

应用级别的 Provide

除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:

js
import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。

event-bus

event-bus 可以让我们轻松实现兄弟组件(不相干组件)之间的数据共享:

dark

在 Vue 中我们推荐使用第三方插件 mitt 帮我们实现兄弟组件之间的数据共享,图示如下:

dark

什么是 event-bus?

event-bus 是一种数据共享的模式的统称:凡是需要创建一个事件对象,并在接收方注册事件、在发送方触发事件的数据共享模式,都叫做“event-bus”。

实现组件的基础布局

Left.vue 组件:

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
const add = () => {
  count.value++
}
</script>

<template>
  <div class="box-left">
    <h5>数据发送方 --- {{ count }}</h5>
    <hr />

    <button @click="add">+</button>
  </div>
</template>

<style scoped>
h5 {
  margin: 0;
  padding: 0;
}
.box-left {
  background-color: orange;
  padding: 15px;
  box-sizing: border-box;
  min-height: 200px;
  flex: 1;
}
</style>

Right.vue 组件:

vue
<script setup>
import { ref } from 'vue'

const num = ref(0)
</script>

<template>
  <div class="box-right">
    <h5>数据接收方 --- {{ num }}</h5>
    <hr />
  </div>
</template>

<style scoped>
h5 {
  margin: 0;
  padding: 0;
}
.box-right {
  background-color: lightblue;
  padding: 15px;
  box-sizing: border-box;
  min-height: 200px;
  flex: 1;
}
</style>

Index.vue 组件:

vue
<script setup>
import Left from './Left.vue'
import Right from './Right.vue'
</script>

<template>
  <div class="app-container">
    <Left />
    <Right />
  </div>
</template>

<style scoped>
.app-container {
  display: flex;
  gap: 10px;
  padding: 10px;
}
</style>

mitt 的基本用法

运行如下的命令,安装 mitt 依赖项:

bash
npm i mitt@3.0.1 -S

在 src 目录下新建 eventBus.js 模块,并创建 emitter 实例:

js
import mitt from 'mitt'
export const emitter = mitt()

在数据接收方 Right.vue 组件中,导入 emitter 并调用 emitter.on() 方法注册事件:

vue
<script setup>
import { ref, onMounted } from 'vue'
import { emitter } from '../eventBus.js'

const num = ref(0)

// 在 onMounted 中注册事件
onMounted(() => {
  emitter.on('share-number', (e) => {
    // 形参中的 e 就是发送过来的数据
    num.value = e
  })
})
</script>

<template>
  <div class="box-right">
    <h5>数据接收方 --- {{ num }}</h5>
    <hr />
  </div>
</template>

<style scoped>
h5 {
  margin: 0;
  padding: 0;
}
.box-right {
  background-color: lightblue;
  padding: 15px;
  box-sizing: border-box;
  min-height: 200px;
  flex: 1;
}
</style>

在数据发送方 Left.vue 组件中,导入 emitter 并调用 emitter.emit() 方法触发事件:

vue
<script setup>
import { ref } from 'vue'
import { emitter } from '../eventBus.js'

const count = ref(0)
const add = () => {
  count.value++
  // 触发事件
  emitter.emit('share-number', count.value)
}
</script>

<template>
  <div class="box-left">
    <h5>数据发送方 --- {{ count }}</h5>
    <hr />

    <button @click="add">+</button>
  </div>
</template>

<style scoped>
h5 {
  margin: 0;
  padding: 0;
}
.box-left {
  background-color: orange;
  padding: 15px;
  box-sizing: border-box;
  min-height: 200px;
  flex: 1;
}
</style>

在组件销毁时解绑事件

改造 Right.vue 组件,抽离公共的事件处理函数,并在 onBeforeUnmount 中调用 emitter.off() 解绑事件处理函数:

vue
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { emitter } from '../eventBus.js'

const num = ref(0)

// 事件回调函数
const shareNumberHandler = (e) => {
  console.log(e)
  // 形参中的 e 就是发送过来的数据
  num.value = e
}

// 在 onMounted 中注册事件
onMounted(() => {
  emitter.on('share-number', shareNumberHandler)
})

// 在 onBeforeUnmount 中解绑事件
onBeforeUnmount(() => {
  emitter.off('share-number', shareNumberHandler)
})
</script>

<template>
  <div class="box-right">
    <h5>数据接收方 --- {{ num }}</h5>
    <hr />
  </div>
</template>

<style scoped>
h5 {
  margin: 0;
  padding: 0;
}
.box-right {
  background-color: lightblue;
  padding: 15px;
  box-sizing: border-box;
  min-height: 200px;
  flex: 1;
}
</style>

为了演示效果,我们在 Index.vue 中通过布尔值 flag 控制 Right.vue 组件的展示和隐藏:

vue
<script setup>
import Left from './components2/Left.vue'
import Right from './components2/Right.vue'
import { ref } from 'vue'

// 基于 flag 控制 Right 组件的展示和隐藏
const flag = ref(true)
</script>

<template>
  <!-- 绑定按钮的 @click 点击事件处理器 -->
  <button @click="flag = !flag">toggle Right</button>
  <hr />
  <div class="app-container">
    <Left />
    <!-- 添加 v-if 指令 -->
    <Right v-if="flag" />
  </div>
</template>

<style scoped>
.app-container {
  display: flex;
  gap: 10px;
  padding: 10px;
}
</style>

pinia*

pinia 是 Vue 官方出品的、用于实现全局状态管理的方案。它的作用如图所示:

dark
  1. pinia 独立与组件之外,负责存储全局共享的数据;

  2. 在 pinia 内部可以根据业务逻辑的不同,把全局数据拆分为独立的 store 单元;

  3. pinia 也遵循单向数据流的思想,保证了数据流向的清晰;

温馨提示

本套视频课主要讲解 Vue3 框架层面的核心内容,将来会单独出一个 pinia 有关的视频教程,为大家讲解 pinia 的完整用法。

天不生夫子,万古长如夜