组件通信 与 ⏳ 生命周期

🧩 组件通信 与 ⏳ 生命周期

作为后端开发者,您已经熟悉模块间的交互和对象生命周期。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>

任务分解:

  1. 创建TodoInput组件(接收输入,emit添加事件)

  2. 创建TodoList组件(接收todos,emit切换/删除事件)

  3. 创建TodoItem组件(显示单个todo)

  4. 创建TodoStats组件(显示统计信息)

  5. 使用Pinia管理状态

  6. 添加数据持久化

💡 要点总结

  1. 组件通信是Vue的核心,根据场景选择合适方式

  2. 生命周期钩子是执行副作用的地方,注意清理工作

  3. 组合式API更灵活 ,推荐使用<script setup>

  4. TypeScript能提高代码质量,尽早使用

  5. 理解数据流:单向数据流是Vue的核心原则

记住:组件通信就像后端服务的API调用,生命周期就像后端服务的启动/关闭钩子。掌握这些概念,您就能构建复杂的前端应用了!

相关推荐
2301_792580002 小时前
xuepso
java·服务器·前端
海绵宝龙2 小时前
Vue中nextTick
前端·javascript·vue.js
天生欧皇张狗蛋2 小时前
前端部署path问题
前端
H_z_q24012 小时前
Web前端制作一个评论发布案例
前端·javascript·css
摘星编程3 小时前
React Native + OpenHarmony:useId唯一标识生成
javascript·react native·react.js
2603_949462103 小时前
Flutter for OpenHarmony社团管理App实战:消息中心实现
android·javascript·flutter
秋秋小事3 小时前
可选链与非空操作符
前端
iRuriCatt3 小时前
智慧景区管理系统 | 计算机毕设项目
java·前端·spring boot·vue·毕设
qq_12498707533 小时前
基于springboot的会议室预订系统设计与实现(源码+论文+部署+安装)
java·vue.js·spring boot·后端·信息可视化·毕业设计·计算机毕业设计