组件通信:props、emit 与 Pinia
组件通信:props、emit 与 Pinia
Vue 组件之间需要通信,根据不同场景有不同方式。这篇说清楚父子组件、跨组件通信的几种方案。
父子组件通信
父传子:props
父组件:
<template>
<ChildComponent
:message="parentMessage"
:count="parentCount"
@child-event="handleChildEvent"
/>
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
const parentMessage = ref('来自父组件的消息')
const parentCount = ref(0)
function handleChildEvent(payload: string) {
console.log('收到子组件事件:', payload)
}
</script>
子组件接收:
<script setup lang="ts">
defineProps<{
message: string
count: number
}>()
const emit = defineEmits<{
(e: 'child-event', payload: string): void
}>()
emit('child-event', '子组件发出的消息')
</script>
子传父:emit
子组件触发事件:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'update', value: number): void
}>()
emit('update', 100)
</script>
父组件监听:
<ChildComponent @update="onUpdate" />
跨级组件通信:provide / inject
多层嵌套时,逐层传 props 很麻烦,用 provide / inject:
祖先组件:
<script setup lang="ts">
import { provide } from 'vue'
provide('userName', '张三')
provide('theme', 'dark')
</script>
后代组件(任意层级):
<script setup lang="ts">
import { inject } from 'vue'
const userName = inject('userName')
const theme = inject('theme', 'light') // 带默认值
</script>
注意:
provide尽量用 Symbol 作为 key 避免冲突- 传递响应式数据时,后代组件能实时拿到最新值
// 新建文件 src/symbols.ts
export const USER_KEY = Symbol('user')
export const THEME_KEY = Symbol('theme')
// 祖先组件
import { USER_KEY } from '../symbols'
provide(USER_KEY, '张三')
// 后代组件
import { USER_KEY } from '../symbols'
const userName = inject(USER_KEY)
用 Pinia 做全局通信
不需要逐层传递,任何组件都能访问:
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const name = ref('')
const token = ref('')
function setUser(userName: string, userToken: string) {
name.value = userName
token.value = userToken
}
return { name, token, setUser }
})
任何组件中使用:
<script setup>
import { useUserStore } from '../stores/user'
const userStore = useUserStore()
// 读取
console.log(userStore.name)
// 修改
userStore.setUser('李四', 'abc123')
</script>
通信方式对比
| 方式 | 适用场景 | 特点 |
|---|---|---|
| props/emit | 父子组件 | 直接、明确 |
| provide/inject | 跨级组件 | 减少prop逐层传递 |
| Pinia | 全局状态 | 任何组件都能访问 |
| v-model | 表单双向绑定 | 语法糖,适合表单 |
| $parent/$children | 极端场景 | 不推荐,耦合高 |
实战:封装一个 Modal 组件
定义 Props 和 Emit
<!-- Modal.vue -->
<script setup lang="ts">
defineProps<{
visible: boolean
title: string
}>()
const emit = defineEmits<{
(e: 'close'): void
}>()
</script>
<template>
<Teleport to="body">
<div v-if="visible" class="modal-mask" @click.self="emit('close')">
<div class="modal-content">
<header>
<h3>{{ title }}</h3>
<button @click="emit('close')">×</button>
</header>
<main>
<slot />
</main>
</div>
</div>
</Teleport>
</template>
使用 Modal
<script setup lang="ts">
const showModal = ref(false)
</script>
<template>
<button @click="showModal = true">打开弹窗</button>
<Modal
:visible="showModal"
title="提示"
@close="showModal = false"
>
<p>这是弹窗内容</p>
</Modal>
</template>
v-if="visible" 控制显示隐藏,@click.self="emit('close')" 点遮罩关闭,Teleport to="body" 把弹窗移到 body 下,避免被父组件样式影响。
总结
- 父子组件:props 向下传递,emit 向上通知
- 跨级组件:provide / inject
- 全局状态:Pinia
- 选型原则:能用 props/emit 解决的就不用 Pinia,能用 Pinia 解决的就不用 Vuex
下篇讲插槽,让组件更灵活。
