在Vue3中,组件通信是构建复杂应用的核心技能。与Vue2相比,Vue3在组件通信方面有了显著的变化和改进。本文将详细介绍Vue3中的各种组件通信方式,并通过实际示例帮助您深入理解。

Vue3组件通信概述

Vue3与Vue2的主要区别

Vue3在组件通信方面进行了重要优化:

  • 移出事件总线:使用mitt库替代

  • 状态管理:Vuex换成了Pinia

  • 双向绑定优化:将.sync优化到v-model中

  • 属性合并:把$listeners合并到$attrs中

  • 移除$children:不再支持$children属性

组件通信方式概览

组件关系 传递方式
父传子 props、v-model、$refs、默认插槽、具名插槽
子传父 props、自定义事件、v-model、$parent、作用域插槽
祖孙组件 $attrs、provide/inject
兄弟/任意组件 mitt、Pinia

1. Props通信

Props是使用频率最高的通信方式,常用于父组件向子组件传递数据。

基本用法

javascript

// 父组件
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>我的车: {{ car }}</h4>
    <h4>儿子给的玩具: {{ toy }}</h4>
    <Child :car="car" :getToy="getToy"/>
  </div>
</template>

<script setup lang="ts">
import Child from './Child.vue'
import { ref } from "vue"

const car = ref('奔驰')
const toy = ref('')

function getToy(value: string) {
  toy.value = value
}
</script>

// 子组件
<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>我的玩具:{{ toy }}</h4>
    <h4>父给我的车:{{ car }}</h4>
    <button @click="getToy(toy)">玩具给父亲</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue"
const toy = ref('奥特曼')

defineProps(['car','getToy'])
</script>

关键点

  • 父传子时,属性值是非函数

  • 子传父时,属性值是函数

  • 使用defineProps在子组件中接收props

2. 自定义事件

自定义事件常用于子组件向父组件通信。

基本概念

原生事件 vs 自定义事件

  • 原生事件

    • 事件名是特定的(click、mouseenter等)

    • 事件对象$event包含事件相关信息

  • 自定义事件

    • 事件名是任意名称

    • 事件对象$event是emit时提供的数据

使用示例

javascript

// 父组件中绑定自定义事件
<Child @send-toy="toy = $event"/>

// 子组件中触发事件
const emit = defineEmits(['send-toy'])
emit('send-toy', '具体数据')

3. Mitt事件总线

Mitt是一个小巧的事件总线库,用于实现任意组件间通信。

安装和配置

bash

npm i mitt

javascript

// src/utils/emitter.ts
import mitt from "mitt"
const emitter = mitt()
export default emitter

使用示例

javascript

// 接收数据的组件
import emitter from "@/utils/emitter"
import { onUnmounted } from "vue"

// 绑定事件
emitter.on('send-toy', (value) => {
  console.log('send-toy事件被触发', value)
})

onUnmounted(() => {
  // 解绑事件
  emitter.off('send-toy')
})

// 提供数据的组件
import emitter from "@/utils/emitter"

function sendToy() {
  // 触发事件
  emitter.emit('send-toy', toy.value)
}

4. v-model双向绑定

v-model在Vue3中得到了显著增强,可以更方便地实现父子组件间的双向绑定。

v-model的本质

html

<!-- 使用v-model指令 -->
<input type="text" v-model="userName">

<!-- v-model的本质 -->
<input type="text" 
  :value="userName" 
  @input="userName = ($event.target as HTMLInputElement).value">

组件上的v-model

javascript

// 父组件
<AtguiguInput v-model="userName"/>

// 等价于
<AtguiguInput 
  :modelValue="userName" 
  @update:model-value="userName = $event"/>

// 子组件
<template>
  <div class="box">
    <input 
      type="text" 
      :value="modelValue" 
      @input="emit('update:model-value', $event.target.value)">
  </div>
</template>

<script setup lang="ts">
defineProps(['modelValue'])
const emit = defineEmits(['update:model-value'])
</script>

多个v-model

javascript

<AtguiguInput 
  v-model:abc="userName" 
  v-model:xyz="password"/>

5. $attrs属性透传

$attrs用于实现父组件向子组件的子组件通信(祖传孙)。

使用示例

javascript

// 父组件
<template>
  <div class="father">
    <Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

// 子组件
<template>
  <div class="child">
    <GrandChild v-bind="$attrs"/>
  </div>
</template>

// 孙组件
<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <h4>a: {{ a }}</h4>
    <h4>b: {{ b }}</h4>
    <h4>c: {{ c }}</h4>
    <h4>d: {{ d }}</h4>
    <h4>x: {{ x }}</h4>
    <h4>y: {{ y }}</h4>
    <button @click="updateA(666)">点我更新A</button>
  </div>
</template>

<script setup lang="ts">
defineProps(['a','b','c','d','x','y','updateA'])
</script>

注意:$attrs会自动排除props中声明的属性。

6. Provide/Inject依赖注入

provide/inject用于实现祖先组件向后代组件通信。

使用示例

javascript

// 祖先组件
<template>
  <div class="father">
    <h4>资产: {{ money }}</h4>
    <h4>汽车: {{ car }}</h4>
    <button @click="money += 1">资产+1</button>
    <button @click="car.price += 1">汽车价格+1</button>
    <Child/>
  </div>
</template>

<script setup lang="ts">
import Child from './Child.vue'
import { ref, reactive, provide } from "vue"

let money = ref(100)
let car = reactive({
  brand: '奔驰',
  price: 100
})

function updateMoney(value: number) {
  money.value += value
}

// 提供数据
provide('moneyContext', { money, updateMoney })
provide('car', car)
</script>

// 后代组件
<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>资产: {{ money }}</h4>
    <h4>汽车: {{ car }}</h4>
    <button @click="updateMoney(6)">点我</button>
  </div>
</template>

<script setup lang="ts">
import { inject } from 'vue'

const { money, updateMoney } = inject('moneyContext', {
  money: ref(0),
  updateMoney: (x: number) => {}
})
const car = inject('car')
</script>

7. 插槽通信

插槽是Vue中强大的内容分发机制,也可以用于组件间通信。

默认插槽

javascript

// 父组件
<Category title="今日热门游戏">
  <ul>
    <li v-for="g in games" :key="g.id">{{ g.name }}</li>
  </ul>
</Category>

// 子组件
<template>
  <div class="item">
    <h3>{{ title }}</h3>
    <slot></slot>
  </div>
</template>

具名插槽

javascript

// 父组件
<Category title="今日热门游戏">
  <template v-slot:s1>
    <ul>
      <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
  </template>
  <template #s2>
    <a href="#">更多</a>
  </template>
</Category>

// 子组件
<template>
  <div class="item">
    <h3>{{ title }}</h3>
    <slot name="s1"></slot>
    <slot name="s2"></slot>
  </div>
</template>

作用域插槽

作用域插槽允许子组件将数据传递给父组件。

javascript

// 父组件
<Game v-slot="params">
  <ul>
    <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
  </ul>
</Game>

// 子组件
<template>
  <div class="category">
    <h2>今日游戏榜单</h2>
    <slot :games="games" a="哈哈"></slot>
  </div>
</template>

<script setup lang="ts">
import { reactive } from 'vue'

let games = reactive([
  {id:'asgdytsa01', name:'英雄联盟'},
  {id:'asgdytsa02', name:'王者荣耀'},
  {id:'asgdytsa03', name:'红色警戒'}
])
</script>

实际应用案例

案例1:模态框组件通信

javascript

// 父组件
<template>
  <div class="app">
    <button @click="showModal = true">打开模态框</button>
    <Modal
      :visible="showModal"
      title="确认删除"
      @confirm="handleConfirm"
      @cancel="handleCancel"
    >
      <p>确定要删除这个项目吗?</p>
    </Modal>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Modal from './components/Modal.vue'

const showModal = ref(false)

const handleConfirm = () => {
  console.log('确认删除')
  showModal.value = false
}

const handleCancel = () => {
  console.log('取消删除')
  showModal.value = false
}
</script>

// Modal组件
<template>
  <div v-if="visible" class="modal-overlay">
    <div class="modal">
      <div class="modal-header">
        <h3>{{ title }}</h3>
        <button @click="$emit('cancel')">×</button>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <button @click="$emit('cancel')">取消</button>
        <button @click="$emit('confirm')">确认</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
defineProps({
  visible: Boolean,
  title: String
})

defineEmits(['confirm', 'cancel'])
</script>

案例2:表单组件v-model

javascript

// 自定义输入组件
<template>
  <div class="custom-input">
    <label v-if="label">{{ label }}</label>
    <input
      :value="modelValue"
      :placeholder="placeholder"
      @input="$emit('update:modelValue', $event.target.value)"
      @blur="$emit('blur')"
      @focus="$emit('focus')"
    />
    <span v-if="error" class="error">{{ error }}</span>
  </div>
</template>

<script setup lang="ts">
defineProps({
  modelValue: String,
  label: String,
  placeholder: String,
  error: String
})

defineEmits(['update:modelValue', 'blur', 'focus'])
</script>

// 使用自定义输入组件
<template>
  <form class="form">
    <CustomInput
      v-model="form.username"
      label="用户名"
      placeholder="请输入用户名"
      :error="errors.username"
    />
    <CustomInput
      v-model="form.email"
      label="邮箱"
      placeholder="请输入邮箱"
      :error="errors.email"
    />
  </form>
</template>

最佳实践与选择指南

如何选择合适的通信方式

  1. 父子组件通信

    • 简单数据传递:使用props

    • 表单类组件:使用v-model

    • 需要访问子组件方法:使用$refs

    • 内容分发:使用插槽

  2. 子父组件通信

    • 简单事件:使用自定义事件

    • 表单双向绑定:使用v-model

    • 需要访问父组件:使用$parent(不推荐)

  3. 跨级组件通信

    • 简单场景:使用$attrs

    • 复杂数据共享:使用provide/inject

    • 全局状态:使用Pinia

  4. 任意组件通信

    • 事件驱动:使用mitt

    • 状态管理:使用Pinia

性能考虑

  • 避免过度使用provide/inject,它会使组件间的依赖关系不明确

  • 对于频繁更新的数据,考虑使用Pinia进行状态管理

  • 合理使用计算属性缓存计算结果

总结

Vue3提供了丰富而灵活的组件通信方式,每种方式都有其适用的场景。掌握这些通信方式,能够帮助您构建出结构清晰、维护性强的Vue应用。

关键要点:

  • Props:父子组件间最基本的数据传递方式

  • 自定义事件:子组件向父组件通信的标准方式

  • v-model:简化表单类组件的双向绑定

  • 插槽:强大的内容分发机制

  • Provide/Inject:跨层级组件通信的利器

  • Mitt:轻量级的事件总线解决方案

  • Pinia:现代化的状态管理库

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐