Skip to content

Vue.js 组件通信

组件通信是 Vue 开发中的核心概念。本章介绍父子组件、兄弟组件以及跨层级组件之间的通信方式。

父子组件通信

Props(父传子)

父组件通过 props 向子组件传递数据。

父组件:

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

const message = ref('Hello from Parent')
const count = ref(10)
</script>

<template>
  <div>
    <h2>父组件</h2>
    <ChildComponent :message="message" :count="count" />
  </div>
</template>

子组件 (ChildComponent.vue):

vue
<script setup>
defineProps({
  message: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})
</script>

<template>
  <div>
    <h3>子组件</h3>
    <p>接收到的消息:{{ message }}</p>
    <p>接收到的数字:{{ count }}</p>
  </div>
</template>

Emits(子传父)

子组件通过触发事件向父组件传递数据。

子组件:

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

function handleClick() {
  emit('update', { id: 1, name: '更新的数据' })
}

function handleDelete() {
  emit('delete', 1)
}
</script>

<template>
  <div>
    <button @click="handleClick">更新</button>
    <button @click="handleDelete">删除</button>
  </div>
</template>

父组件:

vue
<script setup>
import ChildComponent from './ChildComponent.vue'

function handleUpdate(data) {
  console.log('收到更新:', data)
}

function handleDelete(id) {
  console.log('删除 ID:', id)
}
</script>

<template>
  <ChildComponent 
    @update="handleUpdate" 
    @delete="handleDelete" 
  />
</template>

v-model(双向绑定)

v-model 是 props 和 emits 的语法糖。

子组件:

vue
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

function updateValue(event) {
  emit('update:modelValue', event.target.value)
}
</script>

<template>
  <input :value="modelValue" @input="updateValue" />
</template>

父组件:

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

const text = ref('')
</script>

<template>
  <div>
    <CustomInput v-model="text" />
    <p>输入的内容:{{ text }}</p>
  </div>
</template>

多个 v-model

vue
<!-- 子组件 -->
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <div>
    <input 
      :value="firstName" 
      @input="$emit('update:firstName', $event.target.value)"
      placeholder="姓"
    />
    <input 
      :value="lastName" 
      @input="$emit('update:lastName', $event.target.value)"
      placeholder="名"
    />
  </div>
</template>

<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import NameInput from './NameInput.vue'

const firstName = ref('')
const lastName = ref('')
</script>

<template>
  <NameInput v-model:first-name="firstName" v-model:last-name="lastName" />
  <p>全名:{{ firstName }} {{ lastName }}</p>
</template>

Provide / Inject(跨层级通信)

用于祖先组件向后代组件传递数据,无论层级多深。

祖先组件:

vue
<script setup>
import { ref, provide } from 'vue'
import ChildComponent from './ChildComponent.vue'

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

// 提供数据
provide('theme', theme)
provide('user', user)

function toggleTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
</script>

<template>
  <div>
    <button @click="toggleTheme">切换主题</button>
    <ChildComponent />
  </div>
</template>

后代组件(任意层级):

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

// 注入数据
const theme = inject('theme')
const user = inject('user')
</script>

<template>
  <div :class="`theme-${theme}`">
    <p>当前主题:{{ theme }}</p>
    <p>用户:{{ user.name }} ({{ user.role }})</p>
  </div>
</template>

提供默认值

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

// 如果没有提供,使用默认值
const theme = inject('theme', 'light')
const config = inject('config', () => ({ mode: 'default' }))
</script>

模板引用(Template Refs)

父组件可以通过 ref 直接访问子组件的实例。

子组件:

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

const count = ref(0)

function increment() {
  count.value++
}

// 暴露给父组件
defineExpose({
  count,
  increment
})
</script>

<template>
  <div>
    <p>计数:{{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

父组件:

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

const childRef = ref(null)

function accessChild() {
  console.log('子组件的 count:', childRef.value.count)
  childRef.value.increment()
}
</script>

<template>
  <div>
    <ChildComponent ref="childRef" />
    <button @click="accessChild">访问子组件</button>
  </div>
</template>

事件总线(Event Bus)

适用于非父子组件通信(Vue 3 推荐使用状态管理或 provide/inject)。

创建事件总线:

javascript
// eventBus.js
import { ref } from 'vue'

const bus = ref(new Map())

export function useEventBus() {
  function emit(event, ...args) {
    bus.value.get(event)?.forEach(callback => callback(...args))
  }

  function on(event, callback) {
    if (!bus.value.has(event)) {
      bus.value.set(event, [])
    }
    bus.value.get(event).push(callback)
  }

  function off(event, callback) {
    const callbacks = bus.value.get(event)
    if (callbacks) {
      const index = callbacks.indexOf(callback)
      if (index > -1) {
        callbacks.splice(index, 1)
      }
    }
  }

  return { emit, on, off }
}

使用事件总线:

vue
<!-- 组件 A -->
<script setup>
import { useEventBus } from './eventBus'

const { emit } = useEventBus()

function sendMessage() {
  emit('message', 'Hello from Component A')
}
</script>

<template>
  <button @click="sendMessage">发送消息</button>
</template>

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

const { on, off } = useEventBus()

function handleMessage(message) {
  console.log('收到消息:', message)
}

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

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

<template>
  <div>等待消息...</div>
</template>

实战示例:购物车系统

App.vue(父组件):

vue
<script setup>
import { ref, provide } from 'vue'
import ProductList from './ProductList.vue'
import ShoppingCart from './ShoppingCart.vue'

const cart = ref([])

function addToCart(product) {
  const existingItem = cart.value.find(item => item.id === product.id)
  if (existingItem) {
    existingItem.quantity++
  } else {
    cart.value.push({ ...product, quantity: 1 })
  }
}

function removeFromCart(productId) {
  cart.value = cart.value.filter(item => item.id !== productId)
}

function updateQuantity(productId, quantity) {
  const item = cart.value.find(item => item.id === productId)
  if (item) {
    item.quantity = quantity
  }
}

// 提供购物车数据和方法
provide('cart', {
  items: cart,
  addToCart,
  removeFromCart,
  updateQuantity
})
</script>

<template>
  <div class="app">
    <h1>在线商店</h1>
    <div class="container">
      <ProductList />
      <ShoppingCart />
    </div>
  </div>
</template>

<style scoped>
.container {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 20px;
  padding: 20px;
}
</style>

ProductList.vue:

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

const products = ref([
  { id: 1, name: 'iPhone 14', price: 5999 },
  { id: 2, name: 'MacBook Pro', price: 12999 },
  { id: 3, name: 'iPad Air', price: 4399 },
  { id: 4, name: 'AirPods Pro', price: 1999 }
])

const { addToCart } = inject('cart')
</script>

<template>
  <div class="product-list">
    <h2>商品列表</h2>
    <div v-for="product in products" :key="product.id" class="product-card">
      <h3>{{ product.name }}</h3>
      <p class="price">¥{{ product.price }}</p>
      <button @click="addToCart(product)">加入购物车</button>
    </div>
  </div>
</template>

<style scoped>
.product-list {
  padding: 20px;
  background-color: #f5f5f5;
  border-radius: 8px;
}

.product-card {
  padding: 15px;
  margin-bottom: 10px;
  background-color: white;
  border-radius: 4px;
}

.price {
  font-size: 20px;
  font-weight: bold;
  color: #42b983;
  margin: 10px 0;
}

button {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

ShoppingCart.vue:

vue
<script setup>
import { inject, computed } from 'vue'

const { items, removeFromCart, updateQuantity } = inject('cart')

const total = computed(() => {
  return items.value.reduce((sum, item) => {
    return sum + item.price * item.quantity
  }, 0)
})
</script>

<template>
  <div class="shopping-cart">
    <h2>购物车</h2>
    
    <div v-if="items.length === 0" class="empty">
      购物车是空的
    </div>
    
    <div v-else>
      <div v-for="item in items" :key="item.id" class="cart-item">
        <div class="item-info">
          <h4>{{ item.name }}</h4>
          <p>¥{{ item.price }}</p>
        </div>
        <div class="item-actions">
          <input 
            type="number" 
            :value="item.quantity"
            @input="updateQuantity(item.id, Number($event.target.value))"
            min="1"
          />
          <button @click="removeFromCart(item.id)" class="remove-btn">
            删除
          </button>
        </div>
      </div>
      
      <div class="total">
        <strong>总计:¥{{ total }}</strong>
      </div>
      
      <button class="checkout-btn">结算</button>
    </div>
  </div>
</template>

<style scoped>
.shopping-cart {
  padding: 20px;
  background-color: #fff;
  border: 1px solid #ddd;
  border-radius: 8px;
  position: sticky;
  top: 20px;
}

.empty {
  text-align: center;
  color: #999;
  padding: 40px 0;
}

.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #eee;
}

.item-actions {
  display: flex;
  gap: 10px;
  align-items: center;
}

.item-actions input {
  width: 60px;
  padding: 5px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.remove-btn {
  padding: 5px 10px;
  background-color: #f56c6c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.total {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #ddd;
  text-align: right;
  font-size: 18px;
}

.checkout-btn {
  width: 100%;
  margin-top: 15px;
  padding: 12px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
}
</style>

总结

  • Props:父组件向子组件传递数据
  • Emits:子组件向父组件发送事件
  • v-model:实现双向绑定
  • Provide/Inject:跨层级组件通信
  • Template Refs:父组件直接访问子组件
  • Event Bus:兄弟组件或非父子组件通信

选择合适的通信方式:

  • 父子组件:优先使用 Props 和 Emits
  • 跨层级:使用 Provide/Inject
  • 复杂状态:使用 Vuex 或 Pinia

下一步

接下来学习 动态组件,了解如何动态切换组件。