Vue3组件间传值

前言

组件间传值是 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
// composables/useTheme.js
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
npm install mitt
1
2
3
// utils/eventBus.js
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
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
// state
const user = ref(null)
const token = ref(localStorage.getItem('token') || '')

// getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => user.value?.name ?? '游客')

// actions
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
// router.js
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 生成,仅供参考。本文发布于 码上学习,转载请注明出处。