前言
组件间传值是 Vue 日常开发最高频的操作之一。Vue3 提供了丰富的通信方式,本文按使用场景分类,从最基础的父子传值到跨层级的状态管理,一套讲清楚。
一、父传子:props
最基本的传值方式,父组件通过属性向子组件传递数据。
1 2 3 4 5 6 7 8 9 10 11 12
| <!-- 父组件 --> <template> <Child :title="title" :user="user" :visible="true" /> </template>
<script setup> import { ref } from 'vue' import Child from './Child.vue'
const title = ref('Hello') const user = ref({ name: '张三', age: 25 }) </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!-- 子组件 --> <template> <div> <h2>{{ title }}</h2> <p>{{ user.name }} - {{ user.age }}</p> </div> </template>
<script setup> // 不带默认值 const props = defineProps(['title', 'user'])
// 带类型校验和默认值(推荐) const props = defineProps({ title: { type: String, required: true }, user: { type: Object, default: () => ({ name: '', age: 0 }) }, visible: { type: Boolean, default: false } })
// 用 toRefs 解构保持响应性 import { toRefs } from 'vue' const { title, user } = toRefs(props) </script>
|
注意:props 是只读的,子组件不应直接修改 props 的值。
二、子传父:emit
子组件通过事件向父组件传递数据。
1 2 3 4 5 6 7 8 9
| <!-- 父组件 --> <template> <Child @submit="handleSubmit" @cancel="handleCancel" /> </template>
<script setup> const handleSubmit = (data) => { console.log('收到:', data) } const handleCancel = () => { console.log('取消') } </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!-- 子组件 --> <template> <button @click="handleSubmit">提交</button> <button @click="emit('cancel')">取消</button> </template>
<script setup> // 方式一:数组声明(简单场景) const emit = defineEmits(['submit', 'cancel'])
// 方式二:带参数类型校验(推荐) const emit = defineEmits({ submit: (payload) => typeof payload === 'object', cancel: null // 无参数校验 })
const handleSubmit = () => { emit('submit', { id: 1, name: 'hello' }) } </script>
|
Vue3 中 emits 是显式声明的,这有利于 IDE 提示和代码可读性。
三、v-model 双向绑定
Vue3 支持多个 v-model,替代了 Vue2 的 .sync 修饰符。
单个 v-model
1 2 3 4 5 6 7 8 9
| <!-- 父组件 --> <template> <CustomInput v-model="keyword" /> </template>
<script setup> import { ref } from 'vue' const keyword = ref('') </script>
|
1 2 3 4 5 6 7 8 9 10 11 12
| <!-- 子组件 CustomInput.vue --> <template> <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" /> </template>
<script setup> const props = defineProps({ modelValue: String }) const emit = defineEmits(['update:modelValue']) </script>
|
多个 v-model
1 2 3 4 5 6 7 8 9 10 11
| <!-- 父组件 --> <template> <UserForm v-model:name="name" v-model:age="age" v-model:email="email" /> </template>
<script setup> import { ref } from 'vue' const name = ref('') const age = ref(18) const email = ref('') </script>
|
1 2 3 4 5 6 7 8
| <!-- 子组件 --> <script setup> const props = defineProps({ name: String, age: Number, email: String }) const emit = defineEmits(['update:name', 'update:age', 'update:email'])
// 通过 v-model 修饰符修改 props 的值 // emit('update:name', '新值') </script>
|
自定义 v-model 修饰符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!-- 父组件 --> <CustomInput v-model.capitalize="text" />
<!-- 子组件 --> <script setup> const props = defineProps({ modelValue: String, modelModifiers: { default: () => ({}) } }) const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => { let value = e.target.value if (props.modelModifiers.capitalize) { value = value.toUpperCase() } emit('update:modelValue', value) } </script>
|
四、ref 访问子组件
父组件通过 ref 直接访问子组件暴露的数据和方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <!-- 父组件 --> <template> <Child ref="childRef" /> <button @click="getChildData">获取子组件数据</button> </template>
<script setup> import { ref } from 'vue' import Child from './Child.vue'
const childRef = ref(null)
const getChildData = () => { console.log(childRef.value.count) // 子组件暴露的 count childRef.value.focus() // 调用子组件方法 childRef.value.reset() // 调用子组件方法 } </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!-- 子组件 --> <script setup> import { ref } from 'vue'
const count = ref(0) const inputRef = ref(null)
function focus() { inputRef.value?.focus() } function reset() { count.value = 0 }
// 关键:用 defineExpose 暴露给父组件 defineExpose({ count, focus, reset }) </script>
|
Vue3 中 <script setup> 默认封闭,必须用 defineExpose 显式暴露,父组件才能访问。
五、祖孙通信:provide / inject
跨层级传值,无需经过中间组件逐层透传。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!-- 祖先组件 --> <script setup> import { provide, ref, readonly } from 'vue'
const theme = ref('dark') const user = ref({ name: '张三' })
// 提供数据 provide('theme', theme) // 子组件不应直接改 provide 的值,建议提供只读版本 + 修改方法 provide('theme_readonly', readonly(theme)) provide('user', user)
// 提供修改方法 const setTheme = (val) => { theme.value = val } provide('setTheme', setTheme) </script>
|
1 2 3 4 5 6 7 8 9 10
| <!-- 任意子孙组件 --> <script setup> import { inject } from 'vue'
const theme = inject('theme') // undefined 如果没有提供 const theme2 = inject('theme', 'light') // 设置默认值 const setTheme = inject('setTheme')
setTheme('light') // 通过提供的方法修改 </script>
|
封装成组合式函数(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { provide, inject, ref } from 'vue'
const THEME_KEY = Symbol('theme')
export function createTheme() { const theme = ref('light') const setTheme = (val) => { theme.value = val }
provide(THEME_KEY, { theme, setTheme }) return { theme, setTheme } }
export function useTheme() { const context = inject(THEME_KEY) if (!context) throw new Error('useTheme 必须在提供 createTheme 的组件内使用') return context }
|
1 2 3 4 5 6 7 8 9 10 11
| <!-- 根组件 --> <script setup> import { createTheme } from '@/composables/useTheme' const { theme } = createTheme() </script>
<!-- 任意子孙组件 --> <script setup> import { useTheme } from '@/composables/useTheme' const { theme, setTheme } = useTheme() </script>
|
使用 Symbol 作为 key 可避免命名冲突,封装成 composable 后 IDE 有完整类型提示。
六、事件总线(mitt)
Vue3 移除了 $on/$off,轻量级事件可用 mitt 替代。
1 2 3
| import mitt from 'mitt' export const bus = mitt()
|
1 2 3 4 5 6 7 8 9
| <!-- 组件 A:发送 --> <script setup> import { bus } from '@/utils/eventBus'
const sendMsg = () => { bus.emit('global-event', { id: 1, msg: 'hello' }) bus.emit('refresh-data') } </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <!-- 组件 B:监听 --> <script setup> import { bus } from '@/utils/eventBus' import { onMounted, onUnmounted } from 'vue'
onMounted(() => { bus.on('global-event', (payload) => { console.log('收到:', payload) }) bus.on('refresh-data', refreshData) })
// 记得在组件卸载时取消监听,避免内存泄漏 onUnmounted(() => { bus.off('global-event') bus.off('refresh-data', refreshData) }) </script>
<!-- 也可以监听所有事件 --> <script setup> import { onMounted, onUnmounted } from 'vue'
onMounted(() => { bus.on('*', (type, payload) => { console.log(`事件 ${type} 触发:`, payload) }) })
onUnmounted(() => { bus.off('*') }) </script>
|
mitt 适合非父子组件的简单通信,复杂状态管理请用 Pinia。
七、Pinia 状态管理
跨组件、跨页面的共享状态,推荐使用 Pinia。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { defineStore } from 'pinia' import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => { const user = ref(null) const token = ref(localStorage.getItem('token') || '')
const isLoggedIn = computed(() => !!token.value) const userName = computed(() => user.value?.name ?? '游客')
async function login(credentials) { const res = await apiLogin(credentials) token.value = res.token user.value = res.user localStorage.setItem('token', res.token) }
function logout() { user.value = null token.value = '' localStorage.removeItem('token') }
return { user, token, isLoggedIn, userName, login, logout } })
|
1 2 3 4 5 6 7 8 9 10 11 12
| <!-- 任意组件都能读写 store --> <script setup> import { useUserStore } from '@/stores/user' import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 解构响应式状态 const { user, token, isLoggedIn } = storeToRefs(userStore) // actions 直接解构(不是响应式的,不需要 storeToRefs) const { login, logout } = userStore </script>
|
八、路由传参
1 2 3 4 5 6 7 8 9 10
| const routes = [ { path: '/detail/:id', name: 'detail', component: () => import('@/views/Detail.vue'), meta: { requiresAuth: true, title: '详情页' } } ]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!-- 跳转传参 --> <script setup> import { useRouter } from 'vue-router' const router = useRouter()
// query 传参(显示在 URL,刷新不丢失) router.push({ name: 'detail', query: { from: 'list', keyword: 'vue' } })
// params 传参(动态路由参数) router.push({ name: 'detail', params: { id: '123' } })
// state 传参(不在 URL 显示,刷新会丢失) router.push({ name: 'detail', state: { data: { a: 1, b: 2 } } }) </script>
|
1 2 3 4 5 6 7 8 9 10
| <!-- 目标组件接收 --> <script setup> import { useRoute } from 'vue-router' const route = useRoute()
const id = route.params.id // 动态参数 const from = route.query.from // query 参数 const stateData = history.state // state 参数 const title = route.meta.title // 元信息 </script>
|
九、8 种方式对比
| 方式 |
适用场景 |
方向 |
复杂度 |
props |
父→子 |
单向 |
★ |
emit |
子→父 |
单向 |
★ |
v-model |
父子双向绑定 |
双向 |
★★ |
ref + defineExpose |
父主动调用子方法 |
父→子 |
★★ |
provide/inject |
祖→孙(跨层级) |
单向 |
★★ |
mitt 事件总线 |
任意组件间 |
任意 |
★★ |
Pinia |
全局共享状态 |
任意 |
★★★ |
路由传参 |
跨页面 |
页面间 |
★★ |
选型建议
- 父子组件 →
props + emit,表单类加 v-model
- 父调子方法 →
ref + defineExpose
- 跨层级(祖孙) →
provide/inject,封装成 composable
- 兄弟 / 任意组件 → 简单用
mitt,复杂用 Pinia
- 跨页面 → 路由
query/params + Pinia
- 系统级共享 →
Pinia,通过 storeToRefs 保持响应性
内容由 AI 生成,仅供参考。本文发布于 码上学习,转载请注明出处。