组件化开发的核心问题是通信。组件不是孤岛,它们需要相互协作:父组件要传数据给子组件,子组件要通知父组件,兄弟组件之间也要交换信息。
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 | 父 → 子 | 直接访问子组件 |
| children | 双向 | 访问组件实例 |
| provide / inject | 祖先 → 后代 | 跨层级传递 |
| Event Bus | 任意 | 非父子关系 |
| Vuex | 任意 | 全局状态管理 |
单向数据流
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 开发的核心技能: