文章目录
-
- 一、父子通信:Props(父传子)
- [二、父子通信:自定义事件 emit(子传父)](#二、父子通信:自定义事件 emit(子传父))
- 三、跨组件通信:mitt(任意组件通信)
- 四、双向绑定:v-model
- [五、属性透传:attrs(祖给孙传数据)](#五、属性透传:attrs(祖给孙传数据))
- [六、refs 与 parent(直接获取实例)](#六、refs 与 parent(直接获取实例))
-
- [refs(父获取子)](#refs(父获取子))
- [parent(子获取父)](#parent(子获取父))
- [七、Provide / Inject(祖代后代通信)](#七、Provide / Inject(祖代后代通信))
- 八、Pinia(全局状态管理)
- 九、插槽(内容分发)
- 十、通信方式速查表
组件通信的本质: 让不同组件之间能够共享数据或互相通知 。
一、父子通信: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 | 全局 | 多组件共享状态 | 全家共用银行账户 |
| 插槽 | 父 → 子(内容) | 内容分发 | 往盒子里放东西 |
| 作用域插槽 | 子 → 父(数据) | 子提供数据父决定结构 | 子给食材,父做菜 |