Vue3组件通信全方案:provide/inject与自定义事件深入解析

Vue3组件通信方式相比Vue2有了显著变化。Composition API的引入使得跨组件状态共享更加灵活,但同时也增加了方案选型的复杂度。不同场景下应选择不同的通信方式,避免过度使用全局状态管理工具。

父子组件通信:defineProps与defineEmits

父子组件通信是最基础的通信模式。Vue3的script setup语法糖提供了编译器宏defineProps和defineEmits,简化了Props和Events的声明方式。

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const message = ref('Hello from parent')
const count = ref(0)

function handleUpdate(newValue) {
  count.value = newValue
}
</script>

<template>
  <Child
    :message="message"
    :count="count"
    @update="handleUpdate"
  />
</template>

<!-- Child.vue -->
<script setup>
const props = defineProps({
  message: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

const emit = defineEmits(['update'])

function increment() {
  emit('update', props.count + 1)
}
</script>

<template>
  <div>
    <p>{{ message }}</p>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

使用v-model实现双向绑定时,Vue3的语法与Vue2不同。Vue3的v-model默认绑定prop名为modelValue,事件名为update:modelValue。一个组件可以绑定多个v-model。

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const username = ref('')
const email = ref('')
</script>

<template>
  <CustomInput
    v-model="username"
    v-model:email="email"
  />
</template>

<!-- CustomInput.vue -->
<script setup>
const props = defineProps({
  modelValue: String,
  email: String
})

const emit = defineEmits([
  'update:modelValue',
  'update:email'
])

function onInput(e) {
  emit('update:modelValue', e.target.value)
}

function onEmailInput(e) {
  emit('update:email', e.target.value)
}
</script>

<template>
  <input :value="modelValue" @input="onInput" placeholder="用户名" />
  <input :value="email" @input="onEmailInput" placeholder="邮箱" />
</template>

跨层级通信:provide与inject

当组件嵌套层级较深时,通过props逐层传递数据(prop drilling)代码冗余且维护困难。provide/inject允许祖先组件向所有后代组件注入数据,跳过中间层级。

<!-- GrandParent.vue -->
<script setup>
import { ref, provide, readonly } from 'vue'
import Parent from './Parent.vue'

const theme = ref('dark')
const userInfo = ref({ name: '张三', role: 'admin' })

// provide的值可以是响应式引用
// 使用readonly防止后代组件直接修改
provide('theme', theme)
provide('userInfo', readonly(userInfo))

// 提供修改方法而非直接暴露可变引用
function updateTheme(newTheme) {
  theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>

<template>
  <Parent />
</template>

<!-- DeepChild.vue -->
<script setup>
import { inject } from 'vue'

// 第二个参数是默认值,当没有provider提供时使用
const theme = inject('theme', 'light')
const userInfo = inject('userInfo', { name: '', role: 'guest' })
const updateTheme = inject('updateTheme', () => {})

// 尝试修改readonly的值会告警
// userInfo.value.name = '李四' // Vue warning: Set operation on key "name" failed
</script>

<template>
  <div :class="theme">
    <p>用户: {{ userInfo.name }} ({{ userInfo.role }})</p>
    <button @click="updateTheme('light')">切换主题</button>
  </div>
</template>

provide/inject的数据流是单向的,从上到下。通过readonly包装注入的数据并提供修改方法,可以保持数据流的清晰性。这种模式本质上是手动实现的依赖注入,适合主题切换、国际化、用户信息等全局性配置。

兄弟组件通信:事件总线与状态共享

Vue3移除了实例的$on、$emit、$off方法,不再支持通过组件实例创建事件总线。兄弟组件通信的推荐方案是使用第三方库mitt或直接共享响应式状态。

// eventBus.js - 使用mitt创建事件总线
import mitt from 'mitt'

const bus = mitt()

export default bus

// ComponentA.vue - 发送事件
<script setup>
import bus from './eventBus'

function sendData() {
  bus.emit('data-updated', { id: 1, name: '新数据' })
}
</script>

// ComponentB.vue - 接收事件
<script setup>
import { onUnmounted, ref } from 'vue'
import bus from './eventBus'

const receivedData = ref(null)

bus.on('data-updated', (data) => {
  receivedData.value = data
})

// 组件卸载时移除监听,避免内存泄漏
onUnmounted(() => {
  bus.off('data-updated')
})
</script>

<template>
  <p>收到的数据: {{ receivedData }}</p>
</template>

对于需要多组件共享的复杂状态,使用响应式对象共享比事件总线更易于维护。创建一个独立的composable函数封装状态和操作方法,任何组件都可以导入使用。

// useCart.js - 共享购物车状态
import { ref, computed, readonly } from 'vue'

// 模块级别的响应式状态,所有组件共享同一实例
const items = ref([])
const isOpen = ref(false)

export function useCart() {
  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  const count = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  function addItem(product) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  function removeItem(id) {
    items.value = items.value.filter(i => i.id !== id)
  }

  function toggle() {
    isOpen.value = !isOpen.value
  }

  return {
    items: readonly(items),
    isOpen: readonly(isOpen),
    total,
    count,
    addItem,
    removeItem,
    toggle
  }
}

// 任何组件中使用
// import { useCart } from './useCart'
// const { items, total, addItem } = useCart()

使用provide/inject实现类型安全的依赖注入

在TypeScript项目中,provide/inject默认返回any类型,失去了类型检查的优势。通过InjectionKey可以实现类型安全的依赖注入。

// keys.ts
import { InjectionKey, Ref } from 'vue'

interface AppContext {
  theme: Ref<string>;
  user: Ref<{ name: string; role: string }>;
  updateTheme: (theme: string) => void;
}

// 使用InjectionKey定义注入的类型
export const AppContextKey: InjectionKey<AppContext> = Symbol('app-context')

// Provider.vue
import { provide } from 'vue'
import { AppContextKey } from './keys'

provide(AppContextKey, {
  theme,
  user: readonly(user),
  updateTheme
})

// Consumer.vue
import { inject } from 'vue'
import { AppContextKey } from './keys'

// inject会推断出AppContext类型
const ctx = inject(AppContextKey)
if (!ctx) throw new Error('AppContext not provided')

// 完整的TypeScript类型提示
ctx.theme.value   // string
ctx.user.value    // { name: string; role: string }
ctx.updateTheme   // (theme: string) => void

类型安全的依赖注入在大型项目中尤为重要。组件库设计场景下,通过InjectionKey暴露组件API,消费方可以获得完整的IDE类型提示。响应式布局相关的全局配置(如断点阈值、栅格列数)也适合通过provide/inject注入,避免在每个组件中硬编码。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-jian-tong-xin-quan-fang-an-provideinject-yu-zi-ding/

(0)
小编小编
上一篇 12小时前
下一篇 12小时前

相关推荐