目录(快速导航)
- 父 → 子:Props(单向数据流)
- 子 → 父:事件(emit / defineEmits)与 v-model(双向语法糖)
- 父 调 子:ref + defineExpose()(调用子方法 / 访问实例)
- 兄弟组件通信:提升状态 / 父中转 / 共享状态(Pinia) / 事件总线
- 跨层级(祖孙/深层)通信:provide / inject(与响应性注意事项)
- 全局状态管理:Pinia(推荐用于大型或跨多层通信)
- 常见坑、性能与调试建议
- 最佳实践清单与速查表
1 父 → 子:Props(单向数据流)
核心要点: Props 是父组件向子组件传值的官方方式,遵循单向数据流 (父 → 子)。子组件收到的 props 只读(不能直接改),可声明类型、默认值、校验。
示例(<script setup>
)
js
<!-- Child.vue -->
<script setup lang="ts">
// 定义 props(对象语法,推荐)
const props = defineProps({
title: String,
count: { type: Number, default: 0 },
user: { type: Object, default: () => ({}) } // 对象/数组必须用工厂函数返回
})
</script>
<template>
<!-- 使用 props -->
<h2>{{ title }}</h2>
<p>count: {{ count }}</p>
</template>
传递(父组件)
js
<Child :title="post.title" :count="42" :user="{name:'张三'}" />
<!-- 或者绑定整个对象 -->
<Child v-bind="post" />
重要补充 --- Vue 3.5 vs 3.4 及以下(关于「解构 props」的响应性)
- Vue 3.5+:
Vue 的响应系统基于属性访问跟踪状态的使用情况。例如,在计算属性或侦听器中访问 props.foo
时,foo
属性将被跟踪为依赖项。
因此,在以下代码的情况下:
js
const { foo } = defineProps(['foo'])
watchEffect(() => {
// 在 3.5 之前只运行一次
// 在 3.5+ 中在 "foo" prop 变化时重新执行
console.log(foo)
})
- Vue 3.4 及以下:
在 3.4 及以下版本,foo
是一个实际的常量,永远不会改变。在 3.5 及以上版本,当在同一个 <script setup>
代码块中访问由 defineProps
解构的变量时,Vue 编译器会自动在前面添加 props.
。因此,上面的代码等同于以下代码:
scss
const props = defineProps(['foo'])
watchEffect(() => {
// `foo` 由编译器转换为 `props.foo`
console.log(props.foo)
})
2 子 → 父:emit / defineEmits 与
v-model
子组件不能直接改父组件数据,但可以通知父组件:常见方式是触发自定义事件(emit),父组件监听并处理。v-model 在组件上是 props + emit 的语法糖。
2.1 最基础:emit子组件
js
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['ok', 'update']) // 声明可触发的事件(可选但推荐)
function onClick() {
emit('ok', { msg: '子组件通知父组件' })
}
</script>
<template>
<button @click="onClick">通知父组件</button>
</template>
父组件
js
<Child @ok="handleOk" />
<script setup>
function handleOk(payload) {
console.log('收到子组件的事件:', payload)
}
</script>
2.2 v-model(双向绑定语法糖)
- 组件需要接受一个 prop(默认名 modelValue)并发出 update:modelValue 事件;父组件用 v-model 语法双向绑定。
子组件实现(标准)
js
<!-- InputComp.vue -->
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="props.modelValue" @input="onInput" />
</template>
父组件使用
js
<script setup>
import { ref } from 'vue'
const name = ref('')
</script>
<template>
<InputComp v-model="name" />
</template>
2.3 自定义
v-model
名称与多个 v-model
- v-model:propName 可以绑定到自定义 prop(需要子组件接收 propName 并 emit update:propName)。
示例(多个 v-model)
js
<!-- Comp.vue -->
<script setup>
const props = defineProps({
title: String,
count: Number
})
const emit = defineEmits(['update:title','update:count'])
function setTitle(v){ emit('update:title', v) }
function setCount(v){ emit('update:count', v) }
</script>
父组件:
js
<Comp v-model:title="t" v-model:count="n" />
TypeScript 中的写法(defineEmits的类型)
js
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'save', payload: {id:number}): void
}>()
3 父 调 子:ref+defineExpose()(直接调用子组件的方法)
有时父需要调用子组件方法或访问某些实例数据,子组件可以通过 defineExpose 暴露接口。
js
<!-- Child.vue -->
<script setup>
function focusInput() { /* ... */ }
defineExpose({ focusInput })
</script>
父组件:
js
<script setup>
import { ref } from 'vue'
const childRef = ref(null)
function callChild() { childRef.value.focusInput() }
</script>
<template>
<Child ref="childRef" />
<button @click="callChild">调用子方法</button>
</template>
注意: 这种方式打破封装性,应谨慎使用(适合组件库或必要的 API 暴露)。
4 兄弟组件通信
兄弟组件之间没有直接的 props / emit 通道,常见策略:
4.1 提升状态(Lift State Up)
把共享状态提升到最近的公共父组件,父持有状态并把数据通过 props 下发,兄弟通过 emit 通知父更新。
js
SiblingA --(emit)--> Parent (update state) --(props)--> SiblingB
这是最清晰、最推荐的做法(可维护、可追踪)。
4.2 事件总线(不推荐用于大型项目,但小场景可用)
可以用轻量库 mitt 创建全局事件总线,任意组件 emit / on。但是会增加不可见的耦合和调试难度。
js
// event-bus.js
import mitt from 'mitt'
export const bus = mitt()
使用:
js
// 发射
bus.emit('user:change', user)
// 监听
bus.on('user:change', handler)
建议: 对于中大型项目优先使用 Pinia/全局状态或提升状态;仅在确实需要松耦合广播时使用事件总线。
5 跨层级(祖孙/深层)通信:provide/inject
provide/inject 适合跨越多层的依赖注入(例如插件、主题、父级服务等)。
基本用法
js
<!-- Ancestor.vue -->
<script setup>
import { provide, reactive } from 'vue'
const state = reactive({ theme: 'dark' })
provide('appState', state) // 提供响应式对象
</script>
js
<!-- DeepChild.vue -->
<script setup>
import { inject } from 'vue'
const appState = inject('appState')
</script>
响应性注意
- 如果你 provide 一个普通对象(非 reactive/ref),子层 inject 到的就是普通对象,不会自动响应。
- 推荐 provide ref/reactive/computed,或者提供 getter 函数以保证响应性:
js
provide('count', toRef(state, 'count'))
场景适配
- 适合:主题、国际化、表单上下文、可复用组件库内的共享配置。
- 不适合:频繁变更且需要在多人组件之间频繁同步的大规模业务状态 ------ 这时用 Pinia 更合适。
6 全局状态管理:Pinia(推荐用于大型应用)
当状态需要被很多互不相邻的组件读写时,用 Pinia(或 Vuex)会更稳健、可追踪。
简单示例(Pinia)
js
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({ name: '', loggedIn: false }),
actions: {
login(name) { this.name = name; this.loggedIn = true }
}
})
组件中使用:
js
<script setup>
import { useUserStore } from '@/stores/user'
const user = useUserStore()
user.login('张三')
</script>
优点: 单一来源(single source of truth)、DevTools 支持、易于测试与追踪。
7 常见坑、性能与调试建议
常见坑
-
修改 props:子组件不要直接修改 props。若需要本地编辑,复制一份到 ref/reactive。
-
解构 props(版本问题) :3.4 及以下解构会丢失响应性(参见上文)。
-
对象默认值:必须用工厂函数。
-
provide/inject 非响应性误区:记得提供 ref/reactive 保持响应性。
-
过度使用事件总线:会让依赖关系隐式化,调试困难。
性能考虑
-
频繁 emit/事件回流会带来性能开销,避免在短时间内大量事件通信(可合并、去抖)。
-
对大对象频繁深拷贝会影响性能,尽量传引用或只传必要字段。
-
存储大数据建议放在 Pinia/store 并通过 getters/computed 精细化访问。
调试技巧
- 使用 Vue DevTools:查看组件树、props、state、pinia store 的 mutation 历史。
- 控制台警告会指出非法 prop 修改、校验失败、未声明的 emits 等。
- 在开发阶段为组件声明 defineEmits 与 defineProps 的类型,可更早发现问题。
8 最佳实践清单(可直接复制到团队规范)
- Props:总是显式声明并优先使用对象语法或 TypeScript。
- Props 默认值:对象/数组必须使用工厂函数返回。
- 解构 props:考虑兼容性(3.5+ 可直接解构;3.4 及以下用 toRefs)。
- 子→父:优先使用 emit,复杂共享状态使用 Pinia。
- 双向绑定:优先使用 v-model(组件层用 update:xxx 事件)。
- 兄弟通信:优先提升状态到父组件;只有确实必要时才用事件总线。
- 跨层级共享:provide/inject 用于配置/服务类数据;应传递 ref/reactive 保持响应性。
- 暴露 API:若使用 defineExpose,明确文档并限量暴露。
- 命名规范:事件名使用 kebab-case(update:modelValue),prop 使用 camelCase(组件内)/kebab-case(模板)。
- 用 Pinia 管理复杂、跨多层或需要持久化的状态。
9 速查表(一页纸版本)
- 父→子:props / defineProps(只读)
- 子→父:emit / defineEmits(事件)
- 双向:v-model ⇄ prop modelValue + event update:modelValue(或使用 v-model:foo)
- 父调子:ref + defineExpose()
- 兄弟:提升状态(Parent 中转)或 Pinia / 事件总线(谨慎)
- 跨层级:provide(reactive/ref) + inject
- 全局:Pinia(推荐)