组件通信

组件化开发的核心问题是通信。组件不是孤岛,它们需要相互协作:父组件要传数据给子组件,子组件要通知父组件,兄弟组件之间也要交换信息。

Vue 提供了多种通信方式,每种方式都有适用的场景。选择正确的通信方式,能让组件关系清晰、代码易于维护。

为什么需要组件通信

组件封装了独立的功能和状态,但实际应用中,组件之间需要协作:

<!-- 父组件需要把用户数据传给子组件 -->
<user-card :user="currentUser"></user-card>

<!-- 子组件需要通知父组件用户点击了 -->
<button @click="$emit('click')">按钮</button>

<!-- 兄弟组件需要共享状态 -->
<cart-badge></cart-badge>
<cart-list></cart-list>

通信方式概览

Vue 提供了多种通信方式,适用于不同场景:

通信方式方向适用场景
Props父 → 子传递数据
Events ($emit)子 → 父传递事件
v-model双向表单类组件
插槽父 → 子传递模板内容
$refs父 → 子直接访问子组件
parent/parent / children双向访问组件实例
provide / inject祖先 → 后代跨层级传递
Event Bus任意非父子关系
Vuex任意全局状态管理

本章内容

  • Props 传递数据:父组件向子组件传递数据的标准方式
  • 自定义事件 $emit:子组件向父组件发送消息
  • v-model 自定义:实现组件的双向绑定
  • 插槽 Slot:让组件内容更灵活
  • 动态组件:动态切换组件,实现标签页等功能

通信原则

单向数据流

Vue 的数据流是单向的:父组件通过 props 向下传递数据,子组件通过 events 向上发送消息。

父组件
  │
  │ props (向下)
  ▼
子组件
  │
  │ $emit (向上)
  ▼
父组件

这种设计让数据流向清晰,便于追踪和调试。

遵循单向数据流

// ❌ 错误:直接修改 props
props: ['message'],
methods: {
  update() {
    this.message = 'new value'  // 警告
  }
}

// ✅ 正确:通过事件通知父组件修改
props: ['message'],
methods: {
  update() {
    this.$emit('update:message', 'new value')
  }
}

选择通信方式

如何选择合适的通信方式?

父子通信

// 父 → 子:Props
<child :data="parentData"></child>

// 子 → 父:Events
<child @event="handleEvent"></child>

兄弟通信

// 方式1:通过父组件中转
<child-a @change="handleChange"></child-a>
<child-b :data="sharedData"></child-b>

// 方式2:事件总线
bus.$emit('event', data)
bus.$on('event', handler)

跨层级通信

// 方式1:provide/inject
provide: { theme: 'dark' }
inject: ['theme']

// 方式2:Vuex
this.$store.state.xxx

实战示例

父子组件协作

<div id="app">
  <h2>商品列表</h2>
  <product-list 
    :products="products"
    @add-to-cart="addToCart"
  ></product-list>
  
  <h2>购物车</h2>
  <cart 
    :items="cartItems"
    @remove="removeFromCart"
  ></cart>
</div>

<script>
Vue.component('product-list', {
  props: ['products'],
  template: `
    <div class="product-list">
      <div v-for="product in products" :key="product.id" class="product">
        <span>{{ product.name }} - ¥{{ product.price }}</span>
        <button @click="$emit('add-to-cart', product)">加入购物车</button>
      </div>
    </div>
  `
})

Vue.component('cart', {
  props: ['items'],
  template: `
    <div class="cart">
      <div v-for="item in items" :key="item.id" class="cart-item">
        <span>{{ item.name }} - ¥{{ item.price }}</span>
        <button @click="$emit('remove', item)">删除</button>
      </div>
      <p v-if="items.length === 0">购物车是空的</p>
    </div>
  `
})

new Vue({
  el: '#app',
  data: {
    products: [
      { id: 1, name: '商品A', price: 99 },
      { id: 2, name: '商品B', price: 199 },
      { id: 3, name: '商品C', price: 299 }
    ],
    cartItems: []
  },
  methods: {
    addToCart(product) {
      this.cartItems.push(product)
    },
    removeFromCart(item) {
      const index = this.cartItems.findIndex(i => i.id === item.id)
      if (index > -1) {
        this.cartItems.splice(index, 1)
      }
    }
  }
})
</script>

兄弟组件通信

<div id="app">
  <search-input @search="handleSearch"></search-input>
  <search-result :keyword="keyword"></search-result>
</div>

<script>
var bus = new Vue()

Vue.component('search-input', {
  template: `
    <input 
      v-model="text" 
      @keyup.enter="search"
      placeholder="输入关键词搜索"
    >
  `,
  data() {
    return {
      text: ''
    }
  },
  methods: {
    search() {
      bus.$emit('search', this.text)
    }
  }
})

Vue.component('search-result', {
  template: `
    <div>
      <p v-if="keyword">搜索关键词:{{ keyword }}</p>
      <p v-else>请输入搜索关键词</p>
    </div>
  `,
  data() {
    return {
      keyword: ''
    }
  },
  created() {
    bus.$on('search', (keyword) => {
      this.keyword = keyword
    })
  }
})

new Vue({ el: '#app' })
</script>

小结

组件通信是 Vue 开发的核心技能:

  1. 理解单向数据流:props 向下,events 向上
  2. 选择合适方式:根据组件关系选择通信方式
  3. 避免过度耦合:组件应该独立、可复用
  4. 善用状态管理:复杂状态使用 Vuex