Vue3 组件之间的通信方式(子传父 父传子 祖传孙 孙传祖 兄弟 更多后代组件)总结

917 阅读2分钟

image.png

1. Props 和自定义事件(父子通信)

1.1 父传子(Props)

<!-- 父组件 -->
<template>
  <child-component
    :message="message"
    :user-info="userInfo"
  />
</template>

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

const message = ref('Hello from parent')
const userInfo = reactive({
  name: 'John',
  age: 30
})
</script>

<!-- 子组件 -->
<template>
  <div>
    <p>{{ message }}</p>
    <p>{{ userInfo.name }}</p>
  </div>
</template>

<script setup>
defineProps({
  message: String,
  userInfo: Object
})
</script>

1.2 子传父(自定义事件)

<!-- 子组件 -->
<template>
  <button @click="handleClick">更新父组件</button>
</template>

<script setup>
const emit = defineEmits(['update', 'delete'])

const handleClick = () => {
  emit('update', { id: 1, value: 'new value' })
}
</script>

<!-- 父组件 -->
<template>
  <child-component
    @update="handleUpdate"
    @delete="handleDelete"
  />
</template>

<script setup>
const handleUpdate = (data) => {
  console.log('Received from child:', data)
}

const handleDelete = () => {
  // 处理删除逻辑
}
</script>

2. 祖孙组件通信

2.1 provide/inject(跨层级通信)

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

const theme = ref('dark')
const updateTheme = (newTheme) => {
  theme.value = newTheme
}

// 提供响应式数据和更新方法
provide('theme', {
  theme,
  updateTheme
})
</script>

<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

const { theme, updateTheme } = inject('theme')

// 使用注入的数据和方法
const toggleTheme = () => {
  updateTheme(theme.value === 'dark' ? 'light' : 'dark')
}
</script>

2.2 $attrs(透传属性)

<!-- 祖先组件 -->
<template>
  <middle-component
    msg="Hello"
    :user-name="userName"
    @custom-event="handleEvent"
  />
</template>

<!-- 中间子组件 middle-component-->
<template>
  <child-component v-bind="$attrs" />
</template>

<script setup>
// 禁用 attrs 继承
defineOptions({
  inheritAttrs: false
})
</script>

<!-- 孙组件 child-component  -->
<template>
  <div>
    <p>{{ $attrs.msg }}</p>
    <p>{{ $attrs.userName }}</p>
  </div>
</template>

3. 兄弟组件通信

3.1 通过父组件中转

<!-- 父组件 -->
<template>
  <div>
    <sibling-a @message="handleMessage" />
    <sibling-b :message="message" />
  </div>
</template>

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

const message = ref('')
const handleMessage = (value) => {
  message.value = value
}
</script>

<!-- 兄弟组件 A -->
<template>
  <button @click="sendMessage">发送消息</button>
</template>

<script setup>
const emit = defineEmits(['message'])
const sendMessage = () => {
  emit('message', 'Hello from sibling A')
}
</script>

<!-- 兄弟组件 B -->
<template>
  <p>{{ message }}</p>
</template>

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

3.2 使用 mitt 事件总线

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

const sendMessage = () => {
  emitter.emit('message', { text: 'Hello from A' })
}
</script>

<!-- 组件 B -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'

const handleMessage = (data) => {
  console.log('Received:', data)
}

onMounted(() => {
  emitter.on('message', handleMessage)
})

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

4. ref 和 defineExpose

4.1 父组件访问子组件

<!-- 子组件 -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => {
  count.value++
}

// 暴露属性和方法给父组件
defineExpose({
  count,
  increment
})
</script>

<!-- 父组件 -->
<template>
  <child-component ref="childRef" />
  <button @click="accessChild">访问子组件</button>
</template>

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

const childRef = ref(null)

const accessChild = () => {
  console.log(childRef.value.count)
  childRef.value.increment()
}
</script>

4.2 $parent 访问父组件

<!-- 子组件 -->
<template>
  <button @click="accessParent">访问父组件</button>
</template>

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

const { proxy } = getCurrentInstance()

const accessParent = () => {
  // 访问父组件的数据或方法
  console.log(proxy.$parent.parentData)
  proxy.$parent.parentMethod()
}
</script>

5. 插槽通信

5.1 默认插槽

<!-- 父组件 -->
<template>
  <layout-component>
    <p>这是默认插槽内容</p>
  </layout-component>
</template>

<!-- 子组件 -->
<template>
  <div class="container">
    <slot></slot>
  </div>
</template>

5.2 具名插槽

<!-- 父组件 -->
<template>
  <layout-component>
    <template #header>
      <h1>页面标题</h1>
    </template>
    <template #default>
      <main>主要内容</main>
    </template>
    <template #footer>
      <footer>页脚内容</footer>
    </template>
  </layout-component>
</template>

<!-- 子组件 -->
<template>
  <div class="layout">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

5.3 作用域插槽

<!-- 子组件 -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="index">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script setup>
defineProps(['items'])
</script>

<!-- 父组件 -->
<template>
  <list-component :items="items">
    <template #default="{ item, index }">
      <div class="item">
        <span>{{ index + 1 }}.</span>
        <strong>{{ item.name }}</strong>
        <p>{{ item.description }}</p>
      </div>
    </template>
  </list-component>
</template>

6. 最佳实践

  1. 选择合适的通信方式

    • 父子组件:优先使用 props/emit
    • 跨层级组件:考虑 provide/inject
    • 兄弟组件:兄弟组件通信可以使用事件总线或通过父组件中转
    • 临时引用:使用 ref/expose
  2. 数据流向管理

    • 保持单向数据流
    • 避免过度使用全局状态
    • 合理使用响应式数据
  3. 性能优化

    • 避免不必要的组件通信
    • 合理使用计算属性和缓存
    • 及时清理事件监听器
  4. 代码可维护性

    • 明确的命名规范
    • 清晰的数据流向
    • 适当的组件解耦

总结

  1. Props 和自定义事件(父子通信)
  • 父传子:使用 props
  • 子传父:使用自定义事件(emit)
  1. 祖孙组件通信
  • provide/inject:跨层级数据传递
  • $attrs:属性透传
  1. 兄弟组件通信
  • 通过父组件中转
  • 使用 mitt 事件总线
  1. ref 和 defineExpose
  • 父组件访问子组件
  • $parent 访问父组件
  1. 插槽通信
  • 默认插槽
  • 具名插槽
  • 作用域插槽
  1. 最佳实践
  • 选择合适的通信方式
  • 数据流向管理
  • 性能优化
  • 代码可维护性