🧩 组件通信 与 ⏳ 生命周期
作为后端开发者,您已经熟悉模块间的交互和对象生命周期。Vue组件通信和生命周期概念与之类似,让我们系统学习。
🧩 组件通信的8种方式
1. Props 父传子(最常用)
javascript
<!-- 父组件 Parent.vue -->
<template>
<Child
:title="parentTitle"
:user="userData"
:on-change="handleChildChange"
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentTitle = ref('来自父组件的标题')
const userData = ref({ name: '张三', age: 25 })
const handleChildChange = (newData) => {
console.log('子组件传来的数据:', newData)
}
</script>
<!-- 子组件 Child.vue -->
<template>
<div>
<h2>{{ title }}</h2>
<p>{{ user.name }} - {{ user.age }}</p>
<button @click="updateParent">通知父组件</button>
</div>
</template>
<script setup>
// 定义props(推荐用TypeScript)
const props = defineProps({
title: {
type: String,
required: true,
default: '默认标题'
},
user: {
type: Object,
default: () => ({})
},
onChange: {
type: Function,
default: () => {}
}
})
// 触发父组件传递的方法
const updateParent = () => {
props.onChange({ msg: '来自子组件的数据' })
}
</script>
2. Emits 子传父
javascript
<!-- 子组件 Child.vue -->
<template>
<button @click="sendData">发送数据给父组件</button>
<button @click="validate">提交表单</button>
</template>
<script setup>
// 定义emits(Vue 3.3+推荐写法)
const emit = defineEmits<{
// 普通事件
(e: 'message', data: string): void
// 带验证的事件
(e: 'submit', payload: FormData): boolean
// 多个参数
(e: 'update', id: number, value: string): void
}>()
// 触发事件
const sendData = () => {
emit('message', 'Hello from child')
}
const validate = () => {
const formData = { name: '张三', age: 20 }
const isValid = emit('submit', formData)
console.log('验证结果:', isValid)
}
</script>
<!-- 父组件 Parent.vue -->
<template>
<Child
@message="handleMessage"
@submit="handleSubmit"
@update="handleUpdate"
/>
</template>
<script setup>
const handleMessage = (data) => {
console.log('收到子组件消息:', data)
}
const handleSubmit = (formData) => {
console.log('表单数据:', formData)
return true // 返回验证结果
}
const handleUpdate = (id, value) => {
console.log(`更新ID ${id} 为 ${value}`)
}
</script>
3. v-model 双向绑定(语法糖)
javascript
<!-- 子组件 CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 父组件使用 -->
<template>
<!-- 默认用法 -->
<CustomInput v-model="username" />
<!-- 多个v-model -->
<UserForm
v-model:name="formData.name"
v-model:age="formData.age"
v-model:email="formData.email"
/>
</template>
<script setup>
import { ref, reactive } from 'vue'
const username = ref('')
const formData = reactive({
name: '',
age: '',
email: ''
})
</script>
4. Provide / Inject 依赖注入(跨层级)
javascript
<!-- 祖先组件 Ancestor.vue -->
<template>
<Parent>
<Child />
</Parent>
</template>
<script setup>
import { provide, ref, readonly } from 'vue'
// 提供普通数据
provide('theme', 'dark')
// 提供响应式数据
const count = ref(0)
provide('count', count) // 响应式
// 提供只读数据
const config = { api: 'http://api.com' }
provide('config', readonly(config))
// 提供方法
const updateCount = (value) => {
count.value = value
}
provide('updateCount', updateCount)
</script>
<!-- 深层后代组件 Descendant.vue -->
<template>
<div>主题: {{ theme }}</div>
<div>计数: {{ count }}</div>
<button @click="updateCount(count + 1)">增加</button>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme', 'light') // 第二个参数是默认值
const count = inject('count')
const updateCount = inject('updateCount')
// 注入时进行类型断言
const config = inject('config', {}, true) // 设置默认值,并标记为必需
</script>
5. Refs 获取组件实例
javascript
<!-- 父组件 Parent.vue -->
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 获取组件实例
const childRef = ref(null)
onMounted(() => {
console.log('子组件实例:', childRef.value)
console.log('子组件方法:', childRef.value.someMethod)
console.log('子组件数据:', childRef.value.someData)
})
const callChildMethod = () => {
if (childRef.value) {
childRef.value.someMethod()
}
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
<div>子组件</div>
</template>
<script setup>
import { ref, defineExpose } from 'vue'
const someData = ref('内部数据')
const someMethod = () => {
console.log('子组件方法被调用')
}
// 暴露给父组件访问
defineExpose({
someData,
someMethod
})
</script>
6. **事件总线(Event Bus)** - 任意组件通信
javascript
// utils/eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
// 组件A - 发送事件
import { emitter } from '@/utils/eventBus'
const sendEvent = () => {
emitter.emit('user-login', { userId: 123 })
emitter.emit('notification', { type: 'success', message: '操作成功' })
}
// 组件B - 接收事件
import { emitter, onMounted, onUnmounted } from 'vue'
onMounted(() => {
// 监听事件
emitter.on('user-login', (data) => {
console.log('用户登录:', data)
})
// 监听所有事件
emitter.on('*', (type, data) => {
console.log(`事件 ${type}:`, data)
})
})
onUnmounted(() => {
// 清理事件监听
emitter.off('user-login')
emitter.off('*')
})
7. **状态管理(Pinia)** - 复杂应用
javascript
// store/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态
state: () => ({
count: 0,
name: '张三'
}),
// 计算属性
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne(): number {
return this.doubleCount + 1
}
},
// 方法
actions: {
increment() {
this.count++
},
async fetchData() {
const res = await api.getData()
this.name = res.data.name
}
}
})
javascript
<!-- 组件中使用 -->
<template>
<div>{{ store.count }}</div>
<div>{{ doubleCount }}</div>
<button @click="store.increment()">增加</button>
<button @click="store.$reset()">重置</button>
</template>
<script setup>
import { useCounterStore } from '@/store/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// 解构保持响应式
const { count, name, doubleCount } = storeToRefs(store)
// 或者直接使用store属性
console.log(store.count)
store.increment()
</script>
8. 本地存储和全局属性
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 添加全局属性
app.config.globalProperties.$filters = {
formatDate(date) {
return new Date(date).toLocaleDateString()
}
}
// 组件中使用
export default {
mounted() {
console.log(this.$filters.formatDate(new Date()))
}
}
⏳ 组件生命周期
生命周期图示
javascript
创建阶段:
setup() → onBeforeMount() → onMounted()
更新阶段:
onBeforeUpdate() → onUpdated()
卸载阶段:
onBeforeUnmount() → onUnmounted()
激活/停用(KeepAlive):
onActivated() → onDeactivated()
错误处理:
onErrorCaptured()
详细生命周期钩子
javascript
<template>
<div>
<h2>生命周期演示: {{ count }}</h2>
<button @click="count++">更新</button>
<button @click="show = !show">切换子组件</button>
<Child v-if="show" />
</div>
</template>
<script setup>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from 'vue'
import Child from './Child.vue'
const count = ref(0)
const show = ref(true)
// 1. setup - 组合式API的入口
console.log('1. setup - 组件初始化')
// 2. 挂载前
onBeforeMount(() => {
console.log('2. onBeforeMount - 挂载前')
// DOM还未创建,无法操作DOM
})
// 3. 挂载后
onMounted(() => {
console.log('3. onMounted - 挂载完成')
// DOM已创建,可以操作DOM、绑定事件、发起请求
// 常用:获取数据、操作DOM、设置定时器
fetchData()
})
// 4. 更新前
onBeforeUpdate(() => {
console.log('4. onBeforeUpdate - 更新前', count.value)
// 数据已更新,但DOM还未重新渲染
// 可以获取更新前的DOM状态
})
// 5. 更新后
onUpdated(() => {
console.log('5. onUpdated - 更新完成')
// DOM已重新渲染
// 注意:避免在这里修改状态,可能导致无限循环
})
// 6. 卸载前
onBeforeUnmount(() => {
console.log('6. onBeforeUnmount - 卸载前')
// 组件即将销毁
// 清理工作:清除定时器、取消事件监听、取消请求
clearInterval(timer)
window.removeEventListener('resize', handleResize)
})
// 7. 卸载后
onUnmounted(() => {
console.log('7. onUnmounted - 卸载完成')
// 组件已销毁
})
// 8. KeepAlive相关
onActivated(() => {
console.log('onActivated - 组件激活')
// 组件从缓存中激活
})
onDeactivated(() => {
console.log('onDeactivated - 组件停用')
// 组件进入缓存
})
// 9. 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('onErrorCaptured - 捕获错误:', err)
console.log('组件实例:', instance)
console.log('错误信息:', info)
// 返回false阻止错误继续向上传播
return false
})
// 10. 调试钩子(仅开发模式)
onRenderTracked((event) => {
console.log('onRenderTracked - 跟踪渲染依赖:', event)
})
onRenderTriggered((event) => {
console.log('onRenderTriggered - 触发重新渲染:', event)
})
// 异步获取数据示例
const fetchData = async () => {
try {
const res = await fetch('/api/data')
const data = await res.json()
console.log('数据获取成功:', data)
} catch (error) {
console.error('数据获取失败:', error)
}
}
</script>
父子组件生命周期顺序
javascript
<!-- 父组件 Parent.vue -->
<template>
<Child v-if="showChild" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const showChild = ref(true)
onMounted(() => {
console.log('父组件 mounted')
})
</script>
<!-- 子组件 Child.vue -->
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('子组件 mounted')
})
</script>
执行顺序:
javascript
父组件 setup
父组件 onBeforeMount
子组件 setup
子组件 onBeforeMount
子组件 onMounted
父组件 onMounted
🎯 实战:消息通知系统
javascript
<!-- 父组件 App.vue -->
<template>
<div class="app">
<NotificationCenter />
<div class="buttons">
<button @click="sendSuccess">成功消息</button>
<button @click="sendError">错误消息</button>
<button @click="sendWarning">警告消息</button>
</div>
<UserProfile @user-updated="handleUserUpdate" />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import NotificationCenter from './NotificationCenter.vue'
import UserProfile from './UserProfile.vue'
// 1. 使用Provide/Inject共享状态
const notifications = ref([])
const addNotification = (notification) => {
notifications.value.push({
id: Date.now(),
...notification
})
// 3秒后自动移除
setTimeout(() => {
removeNotification(notification.id)
}, 3000)
}
const removeNotification = (id) => {
const index = notifications.value.findIndex(n => n.id === id)
if (index !== -1) {
notifications.value.splice(index, 1)
}
}
provide('notifications', {
list: notifications,
add: addNotification,
remove: removeNotification
})
// 2. 事件通信示例
const sendSuccess = () => {
addNotification({
type: 'success',
message: '操作成功!'
})
}
const sendError = () => {
addNotification({
type: 'error',
message: '发生错误!'
})
}
const sendWarning = () => {
addNotification({
type: 'warning',
message: '警告信息!'
})
}
// 3. 监听子组件事件
const handleUserUpdate = (userData) => {
console.log('用户信息已更新:', userData)
addNotification({
type: 'success',
message: '用户信息已更新'
})
}
</script>
<!-- 消息中心组件 NotificationCenter.vue -->
<template>
<div class="notification-center">
<div
v-for="notification in notifications.list"
:key="notification.id"
:class="['notification', notification.type]"
@click="notifications.remove(notification.id)"
>
{{ notification.message }}
<span class="close">×</span>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
const notifications = inject('notifications')
</script>
<!-- 用户信息组件 UserProfile.vue -->
<template>
<div class="user-profile">
<h3>用户信息</h3>
<input v-model="username" placeholder="用户名" />
<button @click="save">保存</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, defineEmits } from 'vue'
import { emitter } from '@/utils/eventBus'
const emit = defineEmits(['user-updated'])
const username = ref('')
// 生命周期示例
onMounted(() => {
console.log('UserProfile 组件已挂载')
// 加载用户数据
loadUserData()
// 监听全局事件
emitter.on('refresh-user', refreshData)
})
onUnmounted(() => {
console.log('UserProfile 组件即将卸载')
// 清理事件监听
emitter.off('refresh-user')
})
const loadUserData = async () => {
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 1000))
username.value = '张三'
console.log('用户数据加载完成')
}
const refreshData = () => {
console.log('收到刷新事件,重新加载数据')
loadUserData()
}
const save = () => {
console.log('保存用户信息:', username.value)
// 1. 通过emit通知父组件
emit('user-updated', { username: username.value })
// 2. 通过事件总线通知其他组件
emitter.emit('user-saved', { username: username.value })
}
</script>
🔧 通信方式选择指南
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 父子组件简单通信 | Props/Emits | 简单直接,易于理解 |
| 表单双向绑定 | v-model | Vue内置语法糖,简洁 |
| 深层嵌套组件 | Provide/Inject | 避免逐层传递props |
| 任意组件通信 | Pinia/EventBus | 解耦,适合复杂应用 |
| 获取子组件实例 | Refs | 需要调用子组件方法时 |
| 全局配置/方法 | app.config.globalProperties | 工具函数、常量 |
📊 后端开发对比理解
| Vue 概念 | 后端类比 | 说明 |
|---|---|---|
| Props | 函数参数 | 父组件向子组件传递数据 |
| Emits | 回调函数/事件 | 子组件向父组件发送消息 |
| Provide/Inject | 依赖注入 | 类似Spring的@Autowired |
| Pinia | 全局状态/缓存 | 类似Redis或Session存储 |
| 生命周期 | Bean生命周期 | @PostConstruct, @PreDestroy |
| 事件总线 | 消息队列 | 类似RabbitMQ/Kafka |
🎯 最佳实践
1. Props类型验证
javascript
defineProps({
// 基础类型
title: String,
// 多种类型
value: [String, Number],
// 必填
requiredProp: {
type: String,
required: true
},
// 默认值
optionalProp: {
type: Number,
default: 100
},
// 对象/数组的默认值
config: {
type: Object,
default: () => ({ theme: 'light' })
},
// 自定义验证
customProp: {
validator(value) {
return ['success', 'warning', 'error'].includes(value)
}
}
})
2. 生命周期使用场景
javascript
onMounted(() => {
// ✅ 发起数据请求
fetchData()
// ✅ 操作DOM
const el = document.getElementById('target')
// ✅ 添加事件监听
window.addEventListener('resize', handleResize)
// ✅ 设置定时器
timer = setInterval(doSomething, 1000)
})
onBeforeUnmount(() => {
// ✅ 清理定时器
clearInterval(timer)
// ✅ 移除事件监听
window.removeEventListener('resize', handleResize)
// ✅ 取消网络请求
abortController.abort()
})
3. 避免内存泄漏
javascript
import { onMounted, onUnmounted } from 'vue'
export default {
setup() {
let timer
let resizeHandler
onMounted(() => {
timer = setInterval(() => {
console.log('定时器运行中')
}, 1000)
resizeHandler = () => console.log('窗口大小改变')
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
// 必须清理!
clearInterval(timer)
window.removeEventListener('resize', resizeHandler)
})
}
}
📚 调试技巧
javascript
// 1. 在setup中打印生命周期
import { onMounted, getCurrentInstance } from 'vue'
onMounted(() => {
const instance = getCurrentInstance()
console.log('组件实例:', instance)
console.log('组件属性:', instance.props)
console.log('组件上下文:', instance.ctx)
})
// 2. 使用Vue DevTools
// - 安装Chrome扩展
// - 查看组件树
// - 检查Props/Emits
// - 跟踪状态变化
// - 性能分析
🎪 练习题:实现TodoList组件
javascript
<!-- TodoApp.vue -->
<template>
<div>
<h1>Todo List</h1>
<!-- 添加待办 -->
<TodoInput @add="handleAddTodo" />
<!-- 待办列表 -->
<TodoList
:todos="todos"
@toggle="handleToggleTodo"
@delete="handleDeleteTodo"
/>
<!-- 统计信息 -->
<TodoStats :todos="todos" />
</div>
</template>
<script setup>
// 1. 使用Provide/Inject管理todos
// 2. 使用Props/Emits进行组件通信
// 3. 在onMounted中从localStorage加载数据
// 4. 在onBeforeUnmount中保存数据
// 5. 使用事件总线实现通知功能
</script>
任务分解:
-
创建TodoInput组件(接收输入,emit添加事件)
-
创建TodoList组件(接收todos,emit切换/删除事件)
-
创建TodoItem组件(显示单个todo)
-
创建TodoStats组件(显示统计信息)
-
使用Pinia管理状态
-
添加数据持久化
💡 要点总结
-
组件通信是Vue的核心,根据场景选择合适方式
-
生命周期钩子是执行副作用的地方,注意清理工作
-
组合式API更灵活 ,推荐使用
<script setup> -
TypeScript能提高代码质量,尽早使用
-
理解数据流:单向数据流是Vue的核心原则
记住:组件通信就像后端服务的API调用,生命周期就像后端服务的启动/关闭钩子。掌握这些概念,您就能构建复杂的前端应用了!