组件通信: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

下篇讲插槽,让组件更灵活。

最后更新 4/30/2026, 8:57:45 AM