
概述
在 Vue 中,父子组件之间的通信主要有两种方式:
-
$emit:子组件向父组件发送消息(事件驱动) -
$refs:父组件直接访问子组件(直接引用)
一、this.$emit - 子传父的事件机制
1.1 基本概念
$emit 是 Vue 实例方法,用于子组件向父组件触发事件并传递数据。
// 语法
this.$emit(eventName, ...args)
1.2 使用场景
- 子组件需要通知父组件某个事件发生了
- 子组件需要将数据传递给父组件
- 保持组件之间的松耦合
1.3 完整示例
子组件:ProductGrid.vue
<template>
<view class="product-grid">
<view
class="product-item"
v-for="(item, index) in list"
:key="index"
@click="handleItemClick(item)"
>
<image class="product-icon" :src="item.icon" mode="aspectFit" />
<view class="product-title">{{ item.title }}</view>
</view>
</view>
</template>
<script>
export default {
name: "ProductGrid",
props: {
list: {
type: Array,
default: () => []
}
},
methods: {
handleItemClick(item) {
// 触发 itemClick 事件,将 item 数据传递给父组件
this.$emit('itemClick', item)
// 可以传递多个参数
this.$emit('itemClick', item, index, 'extraData')
}
}
}
</script>
父组件:ProductContent.vue
<template>
<view>
<!-- 使用 @itemClick 监听子组件触发的事件 -->
<ProductGrid
:list="productList"
@itemClick="handleItemClick"
/>
</view>
</template>
<script>
import ProductGrid from './ProductGrid.vue'
export default {
components: { ProductGrid },
data() {
return {
productList: [
{ icon: '/static/a.png', title: '产品A' },
{ icon: '/static/b.png', title: '产品B' }
]
}
},
methods: {
// 接收子组件传递的数据
handleItemClick(item, index, extra) {
console.log('点击的产品:', item)
console.log('索引:', index)
console.log('额外数据:', extra)
uni.showToast({
title: item.title,
icon: 'none'
})
}
}
}
</script>
1.4 事件流图解
┌─────────────────┐ 点击事件 ┌─────────────────┐
│ 用户点击产品 │ ──────────────────────> │ handleItemClick │
│ │ │ (子组件方法) │
└─────────────────┘ └────────┬────────┘
│
│ this.$emit('itemClick', item)
▼
┌───────────────┐
│ 触发自定义事件 │
└───────┬───────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ 父组件处理逻辑 │ <────────────────────── │ @itemClick │
│ handleItemClick │ │ (事件监听) │
└─────────────────┘ └─────────────────┘
1.5 最佳实践
// ✅ 好的做法:事件名使用 kebab-case
this.$emit('item-click', item)
// ✅ 好的做法:传递必要的数据
this.$emit('update', { id: 1, name: 'test' })
// ✅ 好的做法:配合 v-model 使用
this.$emit('input', newValue) // 或 update:modelValue (Vue3)
// ❌ 避免:事件名使用驼峰(HTML不区分大小写)
this.$emit('itemClick', item)
二、this.$refs - 直接引用访问
2.1 基本概念
$refs 是 Vue 实例属性,用于父组件直接访问子组件实例或 DOM 元素。
// 语法:通过 ref 名称访问
this.$refs.refName
2.2 使用场景
- 需要调用子组件的方法
- 需要访问子组件的数据
- 需要操作 DOM 元素
- 表单验证等需要直接控制的场景
2.3 完整示例
子组件:Counter.vue
<template>
<view class="counter">
<text>当前计数:{{ count }}</text>
<button @click="increment">+1</button>
</view>
</template>
<script>
export default {
name: "Counter",
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
},
// 父组件可以通过 $refs 调用这个方法
reset() {
this.count = 0
console.log('计数已重置')
},
// 父组件可以通过 $refs 调用这个方法
setValue(value) {
this.count = value
}
}
}
</script>
父组件
<template>
<view>
<!-- 使用 ref 属性给子组件命名 -->
<Counter ref="counterRef" />
<!-- 父组件按钮直接操作子组件 -->
<button @click="resetCounter">重置计数器</button>
<button @click="setCounterTo100">设置为100</button>
<button @click="getCounterValue">获取当前值</button>
</view>
</template>
<script>
import Counter from './Counter.vue'
export default {
components: { Counter },
methods: {
resetCounter() {
// 直接调用子组件的方法
this.$refs.counterRef.reset()
},
setCounterTo100() {
// 调用子组件方法并传参
this.$refs.counterRef.setValue(100)
},
getCounterValue() {
// 直接访问子组件的数据
const currentValue = this.$refs.counterRef.count
console.log('当前计数:', currentValue)
uni.showToast({
title: `当前值:${currentValue}`,
icon: 'none'
})
}
}
}
</script>
2.4 访问 DOM 元素
<template>
<view>
<!-- 给原生元素添加 ref -->
<input ref="inputRef" type="text" placeholder="请输入" />
<button @click="focusInput">聚焦输入框</button>
<button @click="clearInput">清空输入框</button>
</view>
</template>
<script>
export default {
methods: {
focusInput() {
// 在 uni-app 中操作 DOM 需要使用 createSelectorQuery
// 但在 H5 中可以直接访问
this.$refs.inputRef.focus()
},
clearInput() {
this.$refs.inputRef.value = ''
}
}
}
</script>
2.5 注意事项
// ⚠️ $refs 在 mounted 之后才可用
export default {
mounted() {
// ✅ 正确:可以访问 $refs
console.log(this.$refs.counterRef)
},
created() {
// ❌ 错误:此时 DOM 还未渲染
console.log(this.$refs.counterRef) // undefined
}
}
三、emitvsemit vsemitvsrefs 对比
3.1 核心区别表
| 特性 | $emit | $refs |
|---|---|---|
| 通信方向 | 子组件 → 父组件 | 父组件 → 子组件 |
| 触发方式 | 事件驱动(被动) | 直接调用(主动) |
| 使用场景 | 子组件通知父组件 | 父组件控制子组件 |
| 组件耦合 | 松耦合 ✅ | 紧耦合 ⚠️ |
| 代码可读性 | 清晰,易于追踪 | 需要查看多处代码 |
| 测试友好性 | 易于单元测试 | 测试较复杂 |
| TypeScript 支持 | 类型推断友好 | 需要额外类型断言 |
3.2 选择指南
是否需要子组件通知父组件?
├── 是 → 使用 $emit
│ └── 是否需要传递数据?
│ ├── 是 → $emit('event', data)
│ └── 否 → $emit('event')
│
└── 否 → 是否需要父组件调用子组件方法?
├── 是 → 使用 $refs
│ └── 是否需要频繁调用?
│ ├── 是 → 考虑重构,使用 props + $emit
│ └── 否 → 使用 $refs 即可
│
└── 否 → 使用 props 传递数据即可
四、实战案例
4.1 表单验证场景($refs 适用)
<template>
<view>
<Form ref="formRef" :rules="rules" :model="formData">
<FormItem prop="username">
<input v-model="formData.username" placeholder="用户名" />
</FormItem>
<FormItem prop="password">
<input v-model="formData.password" type="password" placeholder="密码" />
</FormItem>
</Form>
<button @click="submitForm">提交</button>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
username: '',
password: ''
},
rules: {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }]
}
}
},
methods: {
async submitForm() {
// 调用子组件的验证方法
const valid = await this.$refs.formRef.validate()
if (valid) {
console.log('验证通过,提交数据:', this.formData)
} else {
console.log('验证失败')
}
}
}
}
</script>
4.2 列表项点击场景($emit 适用)
<!-- 子组件:ProductList.vue -->
<template>
<view class="product-list">
<view
v-for="item in products"
:key="item.id"
class="product-card"
@click="onProductClick(item)"
>
<image :src="item.image" mode="aspectFill" />
<text>{{ item.name }}</text>
</view>
</view>
</template>
<script>
export default {
props: {
products: Array
},
methods: {
onProductClick(item) {
// 通知父组件用户点击了哪个产品
this.$emit('product-click', item)
// 可以同时触发多个事件
this.$emit('track-event', {
event: 'product_click',
productId: item.id
})
}
}
}
</script>
<!-- 父组件 -->
<template>
<ProductList
:products="productList"
@product-click="handleProductClick"
@track-event="handleTrackEvent"
/>
</template>
<script>
export default {
methods: {
handleProductClick(product) {
// 跳转到详情页
uni.navigateTo({
url: `/pages/product/detail?id=${product.id}`
})
},
handleTrackEvent(data) {
// 上报埋点数据
analytics.track(data.event, data)
}
}
}
</script>
五、常见误区
5.1 误区一:过度使用 $refs
// ❌ 不好的做法:用 $refs 获取数据,而不是通过事件
// 子组件
this.$parent.productList // 直接访问父组件数据
// ✅ 正确的做法:通过 props 传递,通过 $emit 通知
// 父组件
<ProductList :products="productList" @update="handleUpdate" />
5.2 误区二:在 created 中使用 $refs
// ❌ 错误
export default {
created() {
this.$refs.myComponent.init() // undefined!
}
}
// ✅ 正确
export default {
mounted() {
this.$refs.myComponent.init() // OK
}
}
5.3 误区三:事件名不规范
// ❌ 不推荐(虽然可以工作,但不规范)
this.$emit('ItemClick', data)
// ✅ 推荐(kebab-case)
this.$emit('item-click', data)
// 模板中
<Item @item-click="handler" />
六、总结
| 景 | 推荐方案 | 原因 |
|---|---|---|
| 子组件通知父组件 | $emit | 松耦合,符合单向数据流 |
| 调用子组件方法 | $refs | 直接、简单 |
| 表单验证 | $refs | 需要同步获取验证结果 |
| 列表项点击 | $emit | 事件驱动,易于扩展 |
| 跨多级组件通信 | provide/inject 或 Vuex | 避免层层传递 |
核心原则:
- 优先使用
$emit进行父子通信 -
$refs只在必要时使用(如调用方法、表单验证) - 避免在
created中使用 $refs - 保持组件松耦合,提高可维护性