Appearance
组件通信
父子组件通信
组件通信是Vue.js应用中最重要的概念之一,它允许组件之间传递数据和事件。Vue.js提供了多种组件通信方式,以满足不同的使用场景。
Props - 父向子传递数据
Props是父组件向子组件传递数据的主要方式。子组件需要显式地声明它期望接收的props。
基础用法
vue
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
props: {
title: {
type: String,
required: true
},
message: {
type: String,
default: 'Default message'
},
count: {
type: Number,
default: 0
}
}
}
</script>
vue
<!-- 父组件 ParentComponent.vue -->
<template>
<div>
<ChildComponent
title="Hello Vue"
:message="parentMessage"
:count="counter"
/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Message from parent',
counter: 5
}
}
}
</script>
</template>
Props 验证
Vue.js提供了丰富的props验证选项:
vue
<script>
export default {
props: {
// 基础类型检查
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数返回
default() {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator(value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].includes(value)
}
},
// 具有默认值的函数
propG: {
type: Function,
default() {
return 'Default function'
}
}
}
}
</script>
事件 - 子向父传递数据
子组件通过$emit方法触发自定义事件,父组件通过v-on监听这些事件来接收数据。
vue
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<p>Child component: {{ childData }}</p>
<button @click="sendDataToParent">Send Data to Parent</button>
<button @click="incrementCounter">Increment</button>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
data() {
return {
childData: 'Data from child'
}
},
methods: {
sendDataToParent() {
// 触发自定义事件,传递数据给父组件
this.$emit('child-event', this.childData, 'additional data')
},
incrementCounter() {
this.$emit('increment')
}
}
}
</script>
vue
<!-- 父组件 ParentComponent.vue -->
<template>
<div>
<p>Parent received: {{ receivedData }}</p>
<p>Counter: {{ counter }}</p>
<ChildComponent
@child-event="handleChildEvent"
@increment="handleIncrement"
/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
data() {
return {
receivedData: '',
counter: 0
}
},
methods: {
handleChildEvent(data, additionalData) {
this.receivedData = `${data} - ${additionalData}`
},
handleIncrement() {
this.counter++
}
}
}
</script>
</template>
组合式API中的组件通信
在组合式API中,组件通信的方式略有不同:
vue
<!-- 子组件 ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
// 定义props
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
// 定义事件发射器
const emit = defineEmits(['update-count', 'send-data'])
const localCount = ref(props.count)
function updateParent() {
// 发射事件
emit('update-count', localCount.value + 1)
emit('send-data', 'Data from child component')
}
function increment() {
localCount.value++
emit('update-count', localCount.value)
}
</script>
<template>
<div>
<h3>{{ title }}</h3>
<p>Count: {{ localCount }}</p>
<button @click="updateParent">Update Parent</button>
<button @click="increment">Increment</button>
</div>
</template>
vue
<!-- 父组件 ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentCount = ref(0)
const receivedData = ref('')
function handleUpdateCount(newCount) {
parentCount.value = newCount
}
function handleReceiveData(data) {
receivedData.value = data
}
</script>
<template>
<div>
<p>Parent Count: {{ parentCount }}</p>
<p>Received: {{ receivedData }}</p>
<ChildComponent
title="Child Component"
:count="parentCount"
@update-count="handleUpdateCount"
@send-data="handleReceiveData"
/>
</div>
</template>
非父子组件通信
事件总线 (Event Bus)
虽然Vue 3不再推荐使用事件总线,但了解其原理仍有助于理解组件通信:
javascript
// eventBus.js
import { createApp } from 'vue'
const eventBus = createApp({}).config.globalProperties
export default eventBus
Provide / Inject
Provide/inject是Vue提供的一种更优雅的跨层级组件通信方式:
vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const user = ref({
name: 'John',
role: 'admin'
})
// 提供响应式数据
provide('theme', theme)
provide('user', user)
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
</script>
<template>
<div>
<button @click="toggleTheme">Toggle Theme</button>
<ChildComponent />
</div>
</template>
vue
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme')
const user = inject('user')
</script>
<template>
<div :class="theme">
<p>Current theme: {{ theme }}</p>
<p>User: {{ user.name }} ({{ user.role }})</p>
</div>
</template>
使用Vuex进行状态管理
对于复杂的应用,推荐使用状态管理库如Vuex:
javascript
// store/index.js
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0,
user: null
},
mutations: {
increment(state) {
state.count++
},
setUser(state, user) {
state.user = user
}
},
actions: {
increment({ commit }) {
commit('increment')
}
},
getters: {
doubleCount: state => state.count * 2
}
})
插槽 (Slots)
插槽是Vue中实现内容分发的重要机制,允许父组件向子组件传递模板内容。
默认插槽
vue
<!-- 子组件 CardComponent.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">Default Header</slot>
</div>
<div class="card-body">
<slot>Default content</slot>
</div>
<div class="card-footer">
<slot name="footer">Default Footer</slot>
</div>
</div>
</template>
vue
<!-- 父组件 -->
<template>
<CardComponent>
<template #header>
<h2>Custom Header</h2>
</template>
<p>This is the main content</p>
<template #footer>
<button>Submit</button>
</template>
</CardComponent>
</template>
作用域插槽
作用域插槽允许子组件向父组件传递数据:
vue
<!-- 子组件 UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user" :index="user.index" :is-admin="user.role === 'admin'">
{{ user.name }}
</slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'user' }
]
}
}
}
</script>
vue
<!-- 父组件 -->
<template>
<UserList>
<template #default="{ user, isAdmin }">
<span :class="{ admin: isAdmin }">
{{ user.name }} - {{ user.role }}
<span v-if="isAdmin">(Admin)</span>
</span>
</template>
</UserList>
</template>
$refs 和 $parent/$children
使用 $refs
vue
<template>
<div>
<input ref="inputRef" type="text">
<ChildComponent ref="childRef" />
<button @click="focusInput">Focus Input</button>
<button @click="callChildMethod">Call Child Method</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
methods: {
focusInput() {
// 访问DOM元素
this.$refs.inputRef.focus()
},
callChildMethod() {
// 调用子组件方法
this.$refs.childRef.childMethod()
}
}
}
</script>
</template>
在组合式API中使用ref:
vue
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const inputRef = ref(null)
const childRef = ref(null)
onMounted(() => {
// 访问DOM元素
inputRef.value.focus()
})
function focusInput() {
inputRef.value.focus()
}
function callChildMethod() {
childRef.value.childMethod()
}
</script>
<template>
<div>
<input ref="inputRef" type="text">
<ChildComponent ref="childRef" />
<button @click="focusInput">Focus Input</button>
<button @click="callChildMethod">Call Child Method</button>
</div>
</template>
组件间通信最佳实践
1. 使用类型化的Props (TypeScript)
vue
<script setup lang="ts">
interface Props {
title: string
count?: number
items?: string[]
callback?: () => void
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [],
callback: () => {}
})
</script>
2. 事件命名规范
vue
<!-- 子组件 -->
<script setup>
const emit = defineEmits<{
'update:modelValue': [value: string]
'custom-event': [data: any]
}>()
function handleClick() {
emit('update:modelValue', 'new value')
emit('custom-event', { message: 'Hello' })
}
</script>
3. 复杂状态管理
对于复杂的应用状态,推荐使用Pinia或Vuex:
vue
<!-- 使用Pinia示例 -->
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 解构会保持响应性
const { user, isLoggedIn } = storeToRefs(userStore)
function login() {
userStore.login()
}
</script>
实际应用示例
表单组件通信
vue
<!-- FormField.vue -->
<script setup>
const props = defineProps({
label: String,
modelValue: [String, Number],
type: {
type: String,
default: 'text'
},
error: String
})
const emit = defineEmits(['update:modelValue'])
function updateValue(event) {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<div class="form-field">
<label>{{ label }}</label>
<input
:type="type"
:value="modelValue"
@input="updateValue"
:class="{ error: error }"
>
<span v-if="error" class="error-message">{{ error }}</span>
</div>
</template>
<style>
.form-field {
margin-bottom: 1rem;
}
.error-message {
color: red;
font-size: 0.8rem;
}
</style>
vue
<!-- 使用表单字段的组件 -->
<script setup>
import { ref, computed } from 'vue'
import FormField from './FormField.vue'
const formData = ref({
email: '',
password: ''
})
const emailError = computed(() => {
if (!formData.value.email) return ''
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(formData.value.email) ? '' : 'Invalid email'
})
const isFormValid = computed(() => {
return formData.value.email &&
emailRegex.test(formData.value.email) &&
formData.value.password.length >= 6
})
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
</script>
<template>
<form>
<FormField
label="Email"
v-model="formData.email"
type="email"
:error="emailError"
/>
<FormField
label="Password"
v-model="formData.password"
type="password"
/>
<button type="submit" :disabled="!isFormValid">Submit</button>
</form>
</template>
组件通信是构建复杂Vue.js应用的基础,通过合理使用各种通信方式,可以构建出结构清晰、易于维护的应用程序。