九、vue3 组件通信:全场景详解

文章目录

组件通信的本质: 让不同组件之间能够共享数据或互相通知

一、父子通信:Props(父传子)

生活例子:爸爸给儿子零花钱。

html 复制代码
<!-- 父组件 Dad.vue -->
<template>
  <div class="dad">
    <h3>爸爸</h3>
    <p>我有 {{ money }} 元</p>
    <Son :pocketMoney="money" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Son from './Son.vue'

const money = ref(100)
</script>
html 复制代码
<!-- 子组件 Son.vue -->
<template>
  <div class="son">
    <h3>儿子</h3>
    <p>爸爸给了我 {{ pocketMoney }} 元</p>
  </div>
</template>

<script setup>
// 声明接收 props
const props = defineProps(['pocketMoney'])

// 使用 props
console.log(props.pocketMoney)  // 100
</script>

关键点

  • defineProps 声明接收的数据
  • props 是只读的,子组件不能修改
  • 可以声明类型和默认值:
ts 复制代码
const props = defineProps({
  pocketMoney: {
    type: Number,
    required: true,
    default: 0
  }
})

二、父子通信:自定义事件 $emit(子传父)

生活例子:儿子向爸爸要更多零花钱。

html 复制代码
<!-- 子组件 Son.vue -->
<template>
  <div class="son">
    <h3>儿子</h3>
    <p>爸爸给了我 {{ pocketMoney }} 元</p>
    <button @click="askForMoney">再要点钱</button>
  </div>
</template>

<script setup>
const props = defineProps(['pocketMoney'])

// 声明要触发的事件
const emit = defineEmits(['askMoney'])

const askForMoney = () => {
  emit('askMoney', 50)  // 触发事件,传 50 元
}
</script>
html 复制代码
<!-- 父组件 Dad.vue -->
<template>
  <div class="dad">
    <h3>爸爸</h3>
    <p>剩余: {{ money }} 元</p>
    <!-- 监听子组件的 askMoney 事件 -->
    <Son :pocketMoney="money" @askMoney="handleAsk" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Son from './Son.vue'

const money = ref(100)

const handleAsk = (amount) => {
  console.log('儿子要', amount, '元')
  money.value -= amount  // 爸爸给钱
}
</script>

关键点

  • defineEmits 声明要触发的事件
  • $emit('事件名', 数据) 触发事件
  • 父组件用 @事件名 监听

三、跨组件通信:mitt(任意组件通信)

生活例子:班级通知群,老师在群里发通知,所有家长都能看到。

bash 复制代码
npm install mitt
ts 复制代码
// utils/emitter.ts
import mitt from 'mitt'

// 创建一个事件总线
const emitter = mitt()

export default emitter
html 复制代码
<!-- 组件 A:发通知的老师 -->
<template>
  <div>
    <h3>老师</h3>
    <button @click="sendNotice">发通知</button>
  </div>
</template>

<script setup>
import emitter from '@/utils/emitter'

const sendNotice = () => {
  emitter.emit('notice', '明天放假!')  // 发通知
}
</script>
html 复制代码
<!-- 组件 B:看通知的家长 -->
<template>
  <div>
    <h3>家长</h3>
    <p>收到通知: {{ notice }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import emitter from '@/utils/emitter'

const notice = ref('暂无通知')

onMounted(() => {
  // 订阅通知
  emitter.on('notice', (msg) => {
    notice.value = msg
  })
})

onUnmounted(() => {
  // 组件销毁时取消订阅,防止内存泄漏!
  emitter.off('notice')
})
</script>

关键点

  • emitter.on('事件名', 回调) 订阅
  • emitter.emit('事件名', 数据) 发布
  • emitter.off('事件名') 取消订阅
  • 必须在 onUnmounted 中取消订阅!

四、双向绑定:v-model

生活例子:你和同桌共享一个笔记本,你写的内容同桌能看到,同桌写的你也能看到。

html 复制代码
<!-- 父组件 -->
<template>
  <div>
    <h3>共享笔记本</h3>
    <p>当前内容: {{ content }}</p>
    <!-- v-model 是 :modelValue + @update:modelValue 的语法糖 -->
    <Child v-model="content" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const content = ref('Hello')
</script>
html 复制代码
<!-- 子组件 Child.vue -->
<template>
  <div>
    <input 
      :value="modelValue" 
      @input="emit('update:modelValue', $event.target.value)" 
    />
  </div>
</template>

<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

v-model 的本质

html 复制代码
<!-- 上面这行 -->
<Child v-model="content" />

<!-- 等价于这行 -->
<Child 
  :modelValue="content" 
  @update:modelValue="content = $event" 
/>

多个 v-model

html 复制代码
<!-- 父组件 -->
<Child v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 子组件 -->
<script setup>
const props = defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])
</script>

五、属性透传:$attrs(祖给孙传数据)

生活例子:爷爷给孙子红包,爸爸只是转手,不拆开看。

html 复制代码
<!-- 爷爷组件 Grandpa.vue -->
<template>
  <Father :gift="1000" message="好好学习" />
</template>
html 复制代码
<!-- 爸爸组件 Father.vue -->
<template>
  <!-- v-bind="$attrs" 把爷爷给的东西全部传给儿子 -->
  <Son v-bind="$attrs" />
</template>

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

const attrs = useAttrs()
console.log(attrs)  // { gift: 1000, message: '好好学习' }
</script>
html 复制代码
<!-- 孙子组件 Son.vue -->
<template>
  <div>
    <p>收到爷爷给的 {{ gift }} 元</p>
    <p>爷爷说: {{ message }}</p>
  </div>
</template>

<script setup>
defineProps(['gift', 'message'])
</script>

关键点

  • $attrs 包含父组件传过来的、但没有通过 props 声明的所有属性
  • v-bind="$attrs" 一次性透传所有属性
  • 适合"中间人"组件只负责转发数据的场景

六、refs 与 parent(直接获取实例)

生活例子:直接喊对方名字,而不是通过传话。

$refs(父获取子)

html 复制代码
<!-- 父组件 -->
<template>
  <div>
    <h3>爸爸</h3>
    <Son ref="sonRef" />
    <button @click="callSon">喊儿子</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Son from './Son.vue'

const sonRef = ref(null)

const callSon = () => {
  // 直接调用儿子的方法
  sonRef.value.sayHello()
  // 直接读取儿子的数据
  console.log(sonRef.value.name)
}
</script>
html 复制代码
<!-- 子组件 Son.vue -->
<template>
  <div>
    <h3>儿子</h3>
  </div>
</template>

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

const name = ref('小明')

const sayHello = () => {
  alert('爸爸好!')
}

// 暴露给父组件使用
defineExpose({ name, sayHello })
</script>

关键点

  • 子组件必须用 defineExpose 暴露属性和方法
  • 父组件通过 ref 获取子组件实例

$parent(子获取父)

html 复制代码
<!-- 子组件 -->
<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()

const callDad = () => {
  // 获取父组件实例
  const parent = instance.parent
  parent.proxy.giveMoney(10)
}
</script>

不推荐频繁使用:破坏了组件的封装性,耦合度太高。

七、Provide / Inject(祖代后代通信)

生活例子:家族信托基金,爷爷设立,孙子、曾孙都能领取。

html 复制代码
<!-- 爷爷组件 Grandpa.vue -->
<template>
  <div>
    <h3>爷爷</h3>
    <Father />
  </div>
</template>

<script setup>
import { provide, ref } from 'vue'
import Father from './Father.vue'

// 提供数据(响应式)
const familyMoney = ref(100000)
provide('familyMoney', familyMoney)

// 提供方法
const giveMoney = (amount) => {
  familyMoney.value -= amount
}
provide('giveMoney', giveMoney)
</script>
html 复制代码
<!-- 爸爸组件 Father.vue -->
<template>
  <div>
    <h3>爸爸(只是路过)</h3>
    <Son />
  </div>
</template>

<script setup>
import Son from './Son.vue'
// 爸爸什么都不用做,数据直接透传给孙子
</script>
html 复制代码
<!-- 孙子组件 Son.vue -->
<template>
  <div>
    <h3>孙子</h3>
    <p>家族基金剩余: {{ money }}</p>
    <button @click="takeMoney">取 1000 元</button>
  </div>
</template>

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

// 注入爷爷提供的数据
const money = inject('familyMoney')
const giveMoney = inject('giveMoney')

const takeMoney = () => {
  giveMoney(1000)
}
</script>

关键点

  • provide('key', value) 提供数据
  • inject('key') 注入数据
  • 可以跨越多层组件(爷爷 → 爸爸 → 儿子 → 孙子)
  • 提供的数据可以是响应式的

八、Pinia(全局状态管理)

生活例子:全家共用一个银行账户,谁取钱、谁存钱都记录在这个账户里。

ts 复制代码
// store/money.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useMoneyStore = defineStore('money', () => {
  // 家庭总存款
  const total = ref(100000)
  
  // 已支出
  const spent = ref(0)
  
  // 剩余
  const remaining = computed(() => total.value - spent.value)
  
  // 支出
  function spend(amount: number) {
    if (amount <= remaining.value) {
      spent.value += amount
    }
  }
  
  // 存入
  function save(amount: number) {
    total.value += amount
  }
  
  return { total, spent, remaining, spend, save }
})
html 复制代码
<!-- 任意组件中使用 -->
<template>
  <div>
    <p>剩余: {{ moneyStore.remaining }}</p>
    <button @click="moneyStore.spend(100)">花 100</button>
    <button @click="moneyStore.save(500)">存 500</button>
  </div>
</template>

<script setup>
import { useMoneyStore } from '@/store/money'

const moneyStore = useMoneyStore()
</script>

九、插槽(内容分发)

生活例子:给你一个空盒子,你可以往里面放任何东西。

默认插槽

html 复制代码
<!-- 父组件 -->
<template>
  <Box>
    <p>我放了一本书 </p>
  </Box>
</template>
html 复制代码
<!-- 子组件 Box.vue -->
<template>
  <div class="box">
    <h4>盒子</h4>
    <!-- 插槽出口:父组件放的内容会显示在这里 -->
    <slot>默认内容(如果父组件没放东西就显示这个)</slot>
  </div>
</template>

具名插槽

html 复制代码
<!-- 父组件 -->
<template>
  <Layout>
    <!-- v-slot:header 的简写是 #header -->
    <template v-slot:header>
      <h1>这是头部</h1>
    </template>
    
    <template #default>
      <p>这是主体内容</p>
    </template>
    
    <template #footer>
      <p>这是底部</p>
    </template>
  </Layout>
</template>
html 复制代码
<!-- 子组件 Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header">默认头部</slot>
    </header>
    <main>
      <slot>默认主体</slot>
    </main>
    <footer>
      <slot name="footer">默认底部</slot>
    </footer>
  </div>
</template>

作用域插槽(数据从子传父)

html 复制代码
<!-- 子组件 List.vue -->
<template>
  <ul>
    <li v-for="item in list" :key="item.id">
      <!-- 把数据传给父组件 -->
      <slot :item="item" :index="index">
        {{ item.name }}  <!-- 默认显示 -->
      </slot>
    </li>
  </ul>
</template>

<script setup>
const list = [
  { id: 1, name: '苹果', price: 5 },
  { id: 2, name: '香蕉', price: 3 },
  { id: 3, name: '橙子', price: 4 }
]
</script>
html 复制代码
<!-- 父组件 -->
<template>
  <List v-slot="{ item, index }">
    <!-- 子组件提供数据,父组件决定怎么显示 -->
    <span>{{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}</span>
  </List>
</template>

作用域插槽的本质:子组件提供数据,父组件决定展示结构。

十、通信方式速查表

方式 方向 场景 记忆口诀
Props 父 → 子 父给子传数据 爸爸给儿子零花钱
$emit 子 → 父 子通知父 儿子向爸爸要钱
mitt 任意 ↔ 任意 跨组件通信 班级群发通知
v-model 双向 表单组件 共享笔记本
$attrs 祖 → 孙 跨级透传属性 爷爷给孙子红包,爸爸转手
$refs 父 → 子 父操作子 爸爸直接喊儿子
provide/inject 祖 → 后代 深层嵌套 家族信托基金
Pinia 全局 多组件共享状态 全家共用银行账户
插槽 父 → 子(内容) 内容分发 往盒子里放东西
作用域插槽 子 → 父(数据) 子提供数据父决定结构 子给食材,父做菜
相关推荐
VOLUN2 小时前
告别 AI 乱码!Vue3+TS 项目的 AI 编码助手规范实践
前端·ai编程
踏雪羽翼2 小时前
android 实现文字打印机效果
android·前端·javascript
编程技术手记2 小时前
Vue Scoped CSS 与动态创建 DOM 的兼容性问题
前端·css·vue.js
Patrick_Wilson2 小时前
从「框架内部报错」到「请求头被网关截断」:一次 Sentry 排障与前端 Cookie 误用复盘
前端·http·浏览器
向上的车轮2 小时前
TypeORM 1.0 正式发布:新一代 Node.js ORM 框架全面解析
typescript·node.js·typeorm
Cerrda2 小时前
从 uno.config.ts 看懂 UnoCSS 图标方案
前端·代码规范
爱勇宝2 小时前
《置身钉内》之后:普通前端的出路在哪里?
前端·后端·程序员
KaMeidebaby3 小时前
卡梅德生物技术快报|羊驼免疫:分子生物学实战:基于羊驼免疫的重链抗体制备与全流程验证方案
前端·网络·数据库·人工智能·算法·百度
MacroZheng3 小时前
别再求前端了!这款对标Claude Design的开源工具,让你一秒拥有顶级设计能力!
前端·vue.js·人工智能