组件通信
1. Props / Emits:最基础的父子传值最基础的父子传值
Props:父传子的单向数据流
Details
这是 Vue 的官方推荐通信方式,遵循单向数据流原则,数据只能从上往下流,事件从下往上传。
适用场景:当你需要把配置、用户信息、状态等数据从父组件传递给子组件时。
vue
<!-- 父组件调用 -->
<script>
// 短评数据
import type { CommentData } from '@/types/comment'
// 准备评论数据 - 现在作为一个对象传递
const commentData = ref<CommentData>({
list: [
{
id: 1,
avatar: 'https://picsum.photos/id/100/300/240',
username: '用户名1',
rating: 4.5,
date: '2025-12-20',
location: '山东',
content: '郑单958可以适应近些年的极端气候,去年种了900亩,今年1200亩!',
likes: 3051,
},
{
id: 2,
avatar: 'https://picsum.photos/id/101/300/240',
username: '用户名2',
rating: 5,
date: '2025-12-21',
location: '河南',
content: '姜还是老的辣,郑单958可以适应近些年的极端气候,去年种了900亩,今年1200亩!',
likes: 2100,
},
{
id: 3,
avatar: 'https://picsum.photos/id/102/300/240',
username: '用户名3',
rating: 3.5,
date: '2025-12-22',
location: '河北',
content: '这个品种确实不错,抗病性强,产量稳定',
likes: 1200,
},
{
id: 4,
avatar: 'https://picsum.photos/id/103/300/240',
username: '用户名4',
rating: 4,
date: '2025-12-23',
location: '江苏',
content: '连续三年种植,效果一直很好',
likes: 980,
},
],
total: 102, // 服务器返回的实际总数
})
</script>
<template>
<DuanPing :comment-data="commentData"></DuanPing>
</template>vue
<!-- 子组件 -->
<script>
import type { CommentData, CommentItem } from '@/types/comment'
// 直接接收-字符串数组形式(最简单)
defineProps(['commentData'])
// 带类型验证
defineProps({
title: String,
count: {
type: Number,
default: 0,
required: true
},
isActive: {
type: Boolean,
default: false
},
user: {
type: Object,
default: () => ({})
}
})
// 官方推荐
// TypeScript 定义 props - 使用明确的接口定义避免类型问题
interface Props {
commentData?: CommentData
title?: string
iconType?: string
showViewAll?: boolean
maxDisplay?: number
}
const props = withDefaults(defineProps<Props>(), {
commentData: undefined, // 不提供默认值,在 computed 中处理
title: '短评',
iconType: 'chat',
showViewAll: true,
maxDisplay: undefined,
})
</script>props 专属坑位
上面的类型声明文件中:
ts
// 评论项接口
export interface CommentItem {
id: number | string
avatar: string
username: string
rating: number
date: string
location: string
content: string
likes: number
}
// 评论数据接口(包含评论列表和总数)
export interface CommentData {
list: CommentItem[]
total: number
}
// 组件 Props 接口
export interface CommentCardProps {
commentData?: CommentData
title?: string
iconType?: string
showViewAll?: boolean
maxDisplay?: number
}其中,绝对不可以有的是:
ts
// 组件 Props 接口
export interface CommentCardProps {
commentData?: CommentData
title?: string
iconType?: string
showViewAll?: boolean
maxDisplay?: number
}因为,interface props 的声明,必须在组件内部,而不可以独立于类型文件中引入使用!否则props为undefined!
为此折腾了俩小时,晕了!
Emits:子传父的事件机制
点击查看详情
适用场景:子组件需要通知父组件有事发生,比如表单提交、按钮点击、输入变化等。
vue
<!-- 子组件 ChildComponent.vue -->
<script>
//定义可触发的事件
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'open'): void
(e: 'close'): void
(e: 'toggle', value: boolean): void
(e: 'click-overlay'): void
(e: 'click-popup'): void
// === 新增:初始高度归位事件 ===
(e: 'initial-height-reset'): void
}
const emit = defineEmits<Emits>()
// 切换弹出层状态
const togglePopup = (show?: boolean) => {
if (isAnimating.value) return
const targetState = show !== undefined ? show : !isActive.value
isAnimating.value = true
isActive.value = targetState
// === 重置拖拽状态 ===
if (targetState) {
resetDragState()
}
// === 新增:弹出层切换时重置初始高度 ===
// 如果弹出层从未激活变为激活(即展开),重置初始高度
if (targetState && isUsingInitialHeight.value) {
resetInitialHeight()
}
// 传递给父组件
emit('update:modelValue', targetState)
emit('toggle', targetState)
if (targetState) {
emit('open')
// 展开后滚动到顶部
setTimeout(() => {
if (scrollViewRef.value) {
scrollViewRef.value.scrollTo({
top: 0,
duration: 0,
})
}
}, 50)
} else {
emit('close')
}
// 动画结束后重置状态
setTimeout(() => {
isAnimating.value = false
}, 300)
}
</script>vue
<!-- 父组件 Parent.vue -->
<tempalate>
<!-- 弹出层组件 -->
<BottomPopup
ref="popupRef"
v-model="popupVisible"
:default-height="80"
:initial-height="100"
active-height="85vh"
@open="handleOpen" <!-- 这里,使用 @ 符接收子组件的 emit -->
@close="handleClose" <!-- 并且指向js逻辑中的具体逻辑 -->
>
<!-- 使用插槽传入自定义内容 -->
<view class="popup-content">
<view class="section">
<text class="title">标题一</text>
<text class="desc">这里是内容描述...</text>
</view>
<view class="section">
<text class="title">标题二</text>
<text class="desc">更多内容...</text>
</view>
<!-- 列表内容 -->
<view
v-for="item in list"
:key="item.id"
class="list-item"
@tap="handleItemClick(item)"
>
<text>{{ item.name }}</text>
</view>
</view>
</BottomPopup>
</tempalate>多组件通信(Vue 3.3+ 推荐)
点击查看详情
vue
<template>
<!-- 使用 v-model 语法糖 -->
<CustomInput v-model="username" />
<!-- 等价于 -->
<CustomInput
:modelValue="username"
@update:modelValue="newValue => username = newValue"
/>
</template>也就是说:
vue
<BottomPopup
ref="popupRef"
v-model="popupVisible"
>
<!-- 插槽内容部分 -->
</BottomPopup>
<!-- 等价于 -->
<BottomPopup
ref="popupRef"
:modelValue="popipVisible"
@uodate:modelValue="newValue => popuopVisible = newValue"
>
<!-- 插槽内容部分 -->
</BottomPopup>2. v-model:双向绑定的语法糖
v-model 在 Vue3 中变得更加强大,支持多个 v-model 绑定
点击查看详情
Vue 2 中的 v-model
vue
<!-- 父组件 -->
<ChildComponent v-model="value" />
<!-- 等价于 -->
<ChildComponent
:value="value"
@input="value = $event"
/>Vue 3 中的默认 v-model
vue
<!-- 父组件 -->
<ChildComponent v-model="value" />
<!-- 等价于 -->
<ChildComponent
:modelValue="value"
@update:modelValue="newValue => value = newValue"
/>但 Vue 3 的 v-model 已经超越了"简单语法糖":
特性一:多个 v-model 绑定
vue
<!-- 父组件:可以绑定多个 v-model -->
<UserForm
v-model:name="userName"
v-model:email="userEmail"
v-model:age="userAge"
/>
<!-- 等价于 -->
<UserForm
:name="userName"
@update:name="userName = $event"
:email="userEmail"
@update:email="userEmail = $event"
:age="userAge"
@update:age="userAge = $event"
/>
<!-- 子组件 UserForm.vue -->
<template>
<input :value="name" @input="updateName($event.target.value)">
<input :value="email" @input="$emit('update:email', $event.target.value)">
<input type="number" :value="age" @input="updateAge($event.target.value)">
</template>
<script setup lang="ts">
// 定义 Props 接口
interface Props {
name: string
email: string
age: number
}
// 定义 Emits 类型 - 传统方式(Vue 3.4 之前):需要手动声明 props 和 emits
const emit = defineEmits<{
(e: 'update:name', value: string): void
(e: 'update:email', value: string): void
(e: 'update:age', value: number): void
}>()
// 接收 Props - 传统方式(Vue 3.4 之前): 需要手动声明 props 和 emits
const props = defineProps<Props>()
// 处理方法
const updateName = (value: string) => {
emit('update:name', value.trim())
}
const updateAge = (value: string) => {
const age = parseInt(value)
if (!isNaN(age)) {
emit('update:age', Math.max(0, age)) // 确保年龄不为负数
}
}
</script>v-model的核心优势:
- 语法简洁,减少样板代码
- 符合双向绑定的直觉
- 支持多个v-model绑定
- 类型安全(配合TypeScript)
适用场景:自定义表单控件(如日期选择器、富文本编辑器)需要双向绑定。
Vue 3.4+:使用 defineModel(不需要手动声明)
Vue 3.4+ 推荐
✅ 新的简洁方式(Vue 3.4+ 推荐):
vue
<!-- ChildComponent.vue -->
<script setup>
// 一行代码搞定!不需要 defineProps 和 defineEmits
const modelValue = defineModel()
// 或者带参数的
const title = defineModel('title')
const count = defineModel('count', { type: Number, default: 0 })
</script>
<template>
<!-- 可以直接用 v-model 绑定 -->
<input v-model="modelValue">
<input v-model="title">
<input v-model="count" type="number">
</template>这样一来,父组件和子组件,都使用 v-modle 绑定!?!?
需要澄清这个重要概念:
- 核心区别:父组件用指令,子组件用变量
vue
<!-- 父组件:使用 v-model 指令 -->
<template>
<ChildComponent v-model="parentData" />
<!-- 或者多个 v-model -->
<ChildComponent
v-model:name="userName"
v-model:age="userAge"
/>
</template>
<!-- 子组件:使用 defineModel() 变量 -->
<script setup>
// 接收父组件的 v-model 绑定
const modelValue = defineModel() // 对应 v-model
const name = defineModel('name') // 对应 v-model:name
const age = defineModel('age', { type: Number })
</script>
<template>
<!-- 在子组件内部使用变量,不是 v-model 指令 -->
<input :value="name" @input="name = $event.target.value">
<!-- 或者直接用 v-model(这是子组件内部的 v-model) -->
<input v-model="age">
</template>- 两个层次的 v-model
层次一:父子通信的 v-model(指令)
vue
<!-- 父组件 template 中 -->
<ChildComponent v-model="data" />
<!-- 这是 Vue 指令,建立父子绑定 -->层次二:子组件内部的 v-model(变量/指令)
vue
<!-- 子组件内部 -->
<script setup>
const data = defineModel() // 这是变量,接收父组件的绑定
</script>
<template>
<!-- 这是子组件内部自己用的 v-model 指令 -->
<input v-model="data"> <!-- 绑定到 defineModel() 返回的变量 -->
</template>- 完整流程示例 - 场景:双向绑定表单
vue
<!-- ParentComponent.vue -->
<template>
<div>
<p>父组件数据:{{ formData.name }} - {{ formData.age }}</p>
<!-- 父组件使用 v-model 指令绑定到子组件 -->
<UserForm
v-model:name="formData.name"
v-model:age="formData.age"
/>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import UserForm from './UserForm.vue'
// 父组件的数据
const formData = reactive({
name: '张三',
age: 25
})
</script>
<!-- UserForm.vue 子组件 -->
<script setup>
// 子组件接收父组件的 v-model 绑定
// 这里 defineModel() 创建的是响应式变量
const name = defineModel('name', { default: '' })
const age = defineModel('age', { type: Number, default: 0 })
</script>
<template>
<div>
<!-- 子组件内部可以像普通变量一样使用 -->
<div>
<label>姓名:</label>
<!-- 方式1:直接绑定到变量 -->
<input :value="name" @input="name = $event.target.value">
</div>
<div>
<label>年龄:</label>
<!-- 方式2:使用子组件内部的 v-model 指令 -->
<input v-model="age" type="number">
</div>
<!-- 显示子组件当前的值 -->
<p>子组件当前值:{{ name }} - {{ age }}</p>
</div>
</template>- 混淆点:子组件模板中的
v-model
vue
<!-- 子组件中这样写: -->
<input v-model="name">
<!-- 这个 v-model 是:
1. 一个 Vue 指令
2. 但它绑定的是 defineModel() 返回的变量
3. 这个变量已经连接了父组件
-->实际上相当于:
vue
<!-- 手动实现的等价代码 -->
<script setup>
// defineModel('name') 相当于:
const props = defineProps(['name'])
const emit = defineEmits(['update:name'])
const name = computed({
get() { return props.name },
set(value) { emit('update:name', value) }
})
</script>
<template>
<!-- 这样就能理解为什么可以用 v-model 了 -->
<input v-model="name">
</template>- 错误理解 vs 正确理解
❌ 错误理解:
vue
<!-- 父组件 -->
<ChildComponent v-model="data" />
<!-- 子组件(错误!) -->
<template>
<!-- 错误:在子组件中对父组件用 v-model -->
<input v-model="parentData">
</template>✅ 正确理解:
vue
<!-- 父组件:使用指令建立连接 -->
<ChildComponent v-model="parentData" />
<!-- 子组件:使用变量接收连接 -->
<script setup>
const modelValue = defineModel() // 接收连接
</script>
<template>
<!-- 内部可以用 v-model 绑定这个变量 -->
<input v-model="modelValue">
<!-- 或者手动绑定 -->
<input
:value="modelValue"
@input="modelValue = $event.target.value"
>
</template>3. Ref / 模板引用:直接操作组件
适用场景:需要调用子组件方法(如弹窗打开)、聚焦输入框、操作原生元素(如 video 播放)。
点击查看详情
vue
<!-- 父组件 Parent.vue -->
<script setup>
import { ref, onMounted, nextTick } from 'vue'
// 创建 - 弹出层引用
const popupRef = ref<InstanceType<typeof BottomPopup> | null>(null)
// 在页面中直接使用onPageScroll生命周期
onPageScroll(e => {
const scrollTopRpx = e.scrollTop / 2
normalizedScrollTop.value = Math.min(100, Math.round(scrollTopRpx))
if (normalizedScrollTop.value > 30) {
ifScroll.value = true
} else {
ifScroll.value = false
}
console.log('🎯 标准化滚动值:', normalizedScrollTop.value)
console.log('🎯 滚动触发布尔值变化:', ifScroll.value)
popupRef.value?.resetInitialHeight()
// popupRef.value?.close()
})
</script>
<tempalate>
<!-- 弹出层组件 -->
<BottomPopup
ref="popupRef" <!-- 这里,使用 ref 进行绑定 -->
v-model="popupVisible"
:default-height="80"
:initial-height="100"
active-height="85vh"
@open="handleOpen"
@close="handleClose"
>
<!-- 使用插槽传入自定义内容 -->
</BottomPopup>
</tempalate>vue
<!-- 子组件 -->
<script>
// === 新增:重置初始高度 ===
const resetInitialHeight = () => {
// 如果已经重置过,或者没有在使用初始高度,直接返回
if (hasInitialHeightReset.value || !isUsingInitialHeight.value) return
console.log('重置弹出层初始高度')
hasInitialHeightReset.value = true
isUsingInitialHeight.value = false
// 触发归位事件
emit('initial-height-reset')
// 注意:这里不需要手动清理事件监听
// 因为使用的是 { once: true } 选项,事件触发后会自动移除
}
// 暴露方法给父组件
defineExpose({
open: () => togglePopup(true),
close: () => togglePopup(false),
toggle: () => togglePopup(),
isActive,
scrollToTop: () => {
if (scrollViewRef.value) {
scrollViewRef.value.scrollTo({
top: 0,
duration: 300,
})
}
},
// === 新增:手动重置初始高度 ===
resetInitialHeight: () => {
resetInitialHeight()
},
// === 新增:获取当前是否在使用初始高度 ===
getIsUsingInitialHeight: () => isUsingInitialHeight.value,
})
</script>2. Provide / Inject:跨层级数据传递
解决"prop 逐级传递"问题,实现祖先与后代组件的直接通信。
适用场景:当数据需要从顶层组件传递到底层组件,中间隔了好几层(比如主题、用户信息、语言设置)。
点击查看详情
vue
<!-- 根组件 App.vue -->
<template>
<div id="app">
<Header />
<div class="main-content">
<Sidebar />
<ContentArea />
</div>
<Footer />
</div>
</template>
<script setup>
import { provide, ref, reactive, computed } from 'vue'
// 提供用户信息
const currentUser = ref({
id: 1,
name: '张三',
role: 'admin',
permissions: ['read', 'write', 'delete']
})
// 提供应用配置
const appConfig = reactive({
theme: 'dark',
language: 'zh-CN',
apiBaseUrl: import.meta.env.VITE_API_URL
})
// 提供方法
const updateUser = (newUserData) => {
currentUser.value = { ...currentUser.value, ...newUserData }
}
const updateConfig = (key, value) => {
appConfig[key] = value
}
// 计算属性
const userPermissions = computed(() => currentUser.value.permissions)
// 提供数据和方法
provide('currentUser', currentUser)
provide('appConfig', appConfig)
provide('updateUser', updateUser)
provide('updateConfig', updateConfig)
provide('userPermissions', userPermissions)
</script>vue
<!-- 深层嵌套的组件 ContentArea.vue -->
<template>
<div class="content-area">
<UserProfile />
<ArticleList />
</div>
</template>
<script setup>
// 这个组件不需要处理 props,直接渲染子组件
</script>vue
<!-- 使用注入的组件 UserProfile.vue -->
<template>
<div class="user-profile">
<h3>用户信息</h3>
<div class="profile-card">
<p>姓名:{{ currentUser.name }}</p>
<p>角色:{{ currentUser.role }}</p>
<p>权限:{{ userPermissions.join(', ') }}</p>
<p>主题:{{ appConfig.theme }}</p>
</div>
<button @click="handleUpdateProfile">更新资料</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据和方法
const currentUser = inject('currentUser')
const appConfig = inject('appConfig')
const userPermissions = inject('userPermissions')
const updateUser = inject('updateUser')
const handleUpdateProfile = () => {
updateUser({
name: '李四',
role: 'user'
})
}
</script>Provide/Inject的优势:
- 避免Props逐层传递的繁琐
- 实现跨层级组件通信
- 提供全局状态和方法的统一管理
- 提高代码的可维护性
##5. Pinia:现代化状态管理
对于复杂应用,Pinia 提供了更优秀的状态管理方案。