组件通信

组件之间怎么传数据?

父子通信

Props 父→子

<!-- Parent.vue -->
<template>
  <Child name="Tom" :age="20" :isActive="true" />
</template>

<script setup>
import Child from './Child.vue'
</script>
<!-- Child.vue -->
<script setup>
defineProps({
  name: String,
  age: {
    type: Number,
    required: true,
    default: 18
  },
  isActive: Boolean
})
</script>

<template>
  <div>{{ name }} - {{ age }} - {{ isActive }}</div>
</template>

Emit 子→父

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', { id: 1, name: 'Tom' })
}
</script>

<template>
  <button @click="handleClick">更新</button>
</template>
<!-- Parent.vue -->
<template>
  <Child @update="handleUpdate" />
</template>

<script setup>
function handleUpdate(data) {
  console.log('收到:', data)
}
</script>

v-model 双向绑定

<!-- Parent.vue -->
<template>
  <Child v-model="count" />
  <!-- 等价于 -->
  <Child :modelValue="count" @update:modelValue="count = $event" />
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<!-- Child.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

多个 v-model

<!-- Parent.vue -->
<template>
  <Child v-model:name="name" v-model:age="age" />
</template>

<script setup>
import { ref } from 'vue'
const name = ref('Tom')
const age = ref(20)
</script>
<!-- Child.vue -->
<script setup>
defineProps(['name', 'age'])
defineEmits(['update:name', 'update:age'])
</script>

Expose / Ref 父调子

<!-- Child.vue -->
<script setup>
function getData() {
  return 'child data'
}

defineExpose({ getData })
</script>

<template>
  <div>Child</div>
</template>
<!-- Parent.vue -->
<template>
  <Child ref="childRef" />
</template>

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

const childRef = ref(null)

onMounted(() => {
  console.log(childRef.value.getData()) // 'child data'
})
</script>

兄弟通信

父组件中转

<!-- Parent.vue -->
<template>
  <A @change="handleChange" :value="value" />
  <B :value="value" />
</template>

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

const value = ref('')

function handleChange(val) {
  value.value = val
}
</script>

Mitt 事件总线

// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
<!-- A.vue -->
<script setup>
import { emitter } from './eventBus'

function send() {
  emitter.emit('message', 'hello from A')
}
</script>
<!-- B.vue -->
<script setup>
import { onUnmounted } from 'vue'
import { emitter } from './eventBus'

emitter.on('message', (msg) => {
  console.log(msg)
})

onUnmounted(() => {
  emitter.off('message')
})
</script>

Pinia 状态管理

// store/useUserStore.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: 'Tom'
  }),
  actions: {
    updateName(name) {
      this.name = name
    }
  }
})
<!-- A.vue -->
<script setup>
import { useUserStore } from './store/useUserStore'

const store = useUserStore()
function change() {
  store.updateName('Jerry')
}
</script>
<!-- B.vue -->
<script setup>
import { useUserStore } from './store/useUserStore'

const store = useUserStore()
</script>

<template>
  <div>{{ store.name }}</div>
</template>

跨层级通信

Provide / Inject

<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'

const count = ref(0)
provide('count', count)

// 也可以传静态值
provide('theme', 'dark')
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

const count = inject('count')
const theme = inject('theme')

function add() {
  count.value++
}
</script>

Provide 响应式(重要!)

// ❌ 非响应式 - 后代拿不到最新值
provide('user', { name: 'Tom' })

// ✅ 响应式 - 用 reactive 或 ref
import { reactive, ref } from 'vue'

const user = reactive({ name: 'Tom' })
provide('user', user)

// ✅ 或者只传 ref
const name = ref('Tom')
provide('name', name)

$attrs 透传

<!-- Parent.vue -->
<template>
  <Child class="custom" style="color: red" data-id="1" />
</template>
<!-- Child.vue -->
<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
console.log(attrs.class) // 'custom'
console.log(attrs.style) // { color: 'red' }
console.log(attrs['data-id']) // '1'
</script>

<template>
  <!-- 透传给孙组件 -->
  <GrandChild v-bind="$attrs" />
</template>

透传 + 拦截

<!-- Child.vue -->
<script setup>
defineProps(['title'])

const attrs = useAttrs()
</script>

<template>
  <div :class="attrs.class">
    <GrandChild
      v-bind="$attrs"
      @click="attrs.onClick"
    />
  </div>
</template>

所有通信方式对比

场景方式备注
父子 Props父→子单向,只读
父子 Emit子→父事件机制
父子 v-model双向绑定语法糖
父子 Ref父调子方法暴露 API
兄弟父组件中转简单场景
兄弟Mitt事件总线
兄弟Pinia状态管理
跨层级Provide/Inject祖先→后代
跨层级$attrs透传
全局Pinia任意组件

最佳实践

  1. 父子通信 → Props + Emit / v-model
  2. 简单兄弟 → 父组件中转
  3. 复杂状态 → Pinia
  4. 跨层级透传 → $attrs
  5. 跨层级共享 → Provide/Inject + reactive