Vue3 组件生命周期详解

597 阅读10分钟

Vue3 组件生命周期详解

核心概念理解

什么是生命周期?

生命周期是指组件从创建销毁的整个过程。在这个过程中,Vue 会在特定的时间点自动调用一些函数,这些函数就是生命周期钩子

为什么要学习生命周期?

  • 在合适的时机执行特定操作
  • 优化性能和资源管理
  • 理解组件的运行机制
  • 调试和排查问题

Vue3 生命周期钩子

1. 创建阶段

<template>
  <div class="lifecycle-demo">
    <h2>生命周期演示</h2>
    <p>计数器: {{ count }}</p>
    <button @click="count++">增加</button>
    <button @click="destroyComponent">销毁组件</button>
    
    <div class="log-section">
      <h3>生命周期日志:</h3>
      <ul class="log-list">
        <li v-for="(log, index) in logs" :key="index">
          {{ log }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

const count = ref(0)
const logs = ref([])
const showComponent = ref(true)

// 记录日志的函数
const addLog = (message) => {
  logs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
  console.log(message)
}

// 1. setup() - 组件创建时(在 <script setup> 中就是整个 setup 函数)
addLog('🔧 setup: 组件开始创建')

// 2. onBeforeMount - 挂载之前
onBeforeMount(() => {
  addLog('🟡 onBeforeMount: 组件即将挂载到 DOM')
  console.log('DOM 还未创建,但数据已经准备好')
})

// 3. onMounted - 挂载之后
onMounted(() => {
  addLog('🟢 onMounted: 组件已挂载到 DOM')
  console.log('DOM 已创建,可以访问 DOM 元素')
  
  // 这里可以进行 DOM 操作、发起网络请求等
  fetchData()
})

// 4. onBeforeUpdate - 更新之前
onBeforeUpdate(() => {
  addLog('🟠 onBeforeUpdate: 组件即将更新')
  console.log('数据已更新,但 DOM 还未更新')
})

// 5. onUpdated - 更新之后
onUpdated(() => {
  addLog('🔵 onUpdated: 组件已更新')
  console.log('DOM 已更新完成')
})

// 6. onBeforeUnmount - 卸载之前
onBeforeUnmount(() => {
  addLog('🟣 onBeforeUnmount: 组件即将卸载')
  console.log('组件即将被销毁,但还存在')
})

// 7. onUnmounted - 卸载之后
onUnmounted(() => {
  addLog('🔴 onUnmounted: 组件已卸载')
  console.log('组件已被销毁,清理工作')
  
  // 清理定时器、事件监听器等
  cleanup()
})

// 模拟异步数据获取
const fetchData = () => {
  console.log('发起网络请求...')
  // 模拟 API 调用
  setTimeout(() => {
    console.log('数据获取完成')
  }, 1000)
}

// 清理工作
const cleanup = () => {
  console.log('执行清理工作')
  // 清理定时器、取消网络请求、移除事件监听器等
}

// 销毁组件
const destroyComponent = () => {
  showComponent.value = false
}
</script>

<style>
.lifecycle-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

button {
  padding: 8px 16px;
  margin: 5px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

button:hover {
  background-color: #0056b3;
}

.log-section {
  margin-top: 30px;
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #dee2e6;
}

.log-section h3 {
  margin-top: 0;
  color: #495057;
}

.log-list {
  max-height: 300px;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-radius: 4px;
  padding: 10px;
  margin: 0;
  font-size: 14px;
}

.log-list li {
  padding: 5px 0;
  border-bottom: 1px solid #eee;
}

.log-list li:last-child {
  border-bottom: none;
}
</style>

详细生命周期阶段

1. 创建阶段 (Creation)

<template>
  <div class="creation-stage">
    <h2>创建阶段演示</h2>
    <p>组件状态: {{ componentState }}</p>
    <div class="data-display">
      <p>数据初始化: {{ initializedData }}</p>
      <p>计算属性: {{ computedValue }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onBeforeMount, onMounted } from 'vue'

const componentState = ref('setup 阶段')
const initializedData = ref('未初始化')
const logs = ref([])

// setup 阶段 - 组件创建时
console.log('🔧 setup 阶段: 组件开始创建')
componentState.value = 'setup 阶段'

// 初始化数据
const initializeData = () => {
  console.log('正在初始化数据...')
  initializedData.value = '数据已初始化'
  logs.value.push('数据初始化完成')
}

// 计算属性
const computedValue = computed(() => {
  console.log('计算属性被访问')
  return `计算结果: ${initializedData.value}`
})

// onBeforeMount - 挂载之前
onBeforeMount(() => {
  console.log('🟡 onBeforeMount: 组件即将挂载')
  componentState.value = '即将挂载'
  initializeData()
  
  // 此时可以访问响应式数据,但不能访问 DOM
  console.log('准备挂载的数据:', initializedData.value)
})

// onMounted - 挂载之后
onMounted(() => {
  console.log('🟢 onMounted: 组件已挂载')
  componentState.value = '已挂载'
  
  // 此时可以访问 DOM 元素
  console.log('DOM 已创建,可以进行 DOM 操作')
  
  // 常见用途:
  // 1. 发起网络请求
  // 2. 访问 DOM 元素
  // 3. 启动定时器
  // 4. 添加事件监听器
})
</script>

<style>
.creation-stage {
  padding: 20px;
  background-color: #e3f2fd;
  border-radius: 8px;
  margin: 20px 0;
}

.data-display {
  margin-top: 20px;
  padding: 15px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #bbdefb;
}

.data-display p {
  margin: 10px 0;
}
</style>

2. 更新阶段 (Update)

<template>
  <div class="update-stage">
    <h2>更新阶段演示</h2>
    
    <div class="controls">
      <button @click="updateData">更新数据</button>
      <button @click="triggerReactiveUpdate">触发响应式更新</button>
      <input v-model="inputValue" placeholder="输入文字触发更新" class="update-input">
    </div>
    
    <div class="data-section">
      <h3>当前数据:</h3>
      <p>计数器: {{ counter }}</p>
      <p>输入值: {{ inputValue }}</p>
      <p>计算值: {{ doubleCounter }}</p>
    </div>
    
    <div class="log-section">
      <h3>更新日志:</h3>
      <ul class="log-list">
        <li v-for="(log, index) in updateLogs" :key="index">
          {{ log }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onBeforeUpdate, onUpdated, watch } from 'vue'

const counter = ref(0)
const inputValue = ref('')
const updateLogs = ref([])

// 计算属性
const doubleCounter = computed(() => {
  console.log('计算属性重新计算')
  return counter.value * 2
})

// 记录更新日志
const addUpdateLog = (message) => {
  updateLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
  console.log(message)
}

// onBeforeUpdate - 更新之前
onBeforeUpdate(() => {
  addUpdateLog('🟠 onBeforeUpdate: 数据即将更新')
  console.log('响应式数据已变化,但 DOM 还未更新')
  console.log('当前计数器值:', counter.value)
})

// onUpdated - 更新之后
onUpdated(() => {
  addUpdateLog('🔵 onUpdated: DOM 已更新')
  console.log('DOM 更新完成')
  console.log('更新后的计数器值:', counter.value)
})

// 更新数据
const updateData = () => {
  counter.value++
}

const triggerReactiveUpdate = () => {
  // 触发多次更新来观察生命周期
  counter.value += 1
  setTimeout(() => {
    counter.value += 1
  }, 100)
}

// 监听数据变化
watch(counter, (newVal, oldVal) => {
  console.log(`计数器从 ${oldVal} 变为 ${newVal}`)
})

watch(inputValue, (newVal) => {
  console.log('输入值变化:', newVal)
})
</script>

<style>
.update-stage {
  padding: 20px;
  background-color: #f3e5f5;
  border-radius: 8px;
  margin: 20px 0;
}

.controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 20px;
  align-items: center;
}

.update-input {
  padding: 8px 12px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 14px;
}

button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #7e57c2;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

button:hover {
  background-color: #5e35b1;
}

.data-section {
  margin: 20px 0;
  padding: 15px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ce93d8;
}

.data-section h3 {
  margin-top: 0;
  color: #495057;
}

.log-section {
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ce93d8;
}

.log-list {
  max-height: 200px;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-radius: 4px;
  padding: 10px;
  margin: 0;
  font-size: 14px;
}
</style>

3. 销毁阶段 (Destruction)

<template>
  <div class="destruction-stage">
    <h2>销毁阶段演示</h2>
    
    <div class="component-controls">
      <button @click="toggleComponent" class="toggle-btn">
        {{ showChild ? '销毁子组件' : '创建子组件' }}
      </button>
    </div>
    
    <!-- 条件渲染子组件 -->
    <div v-if="showChild" class="child-container">
      <LifecycleChild 
        @child-mounted="handleChildMounted"
        @child-unmounted="handleChildUnmounted"
      />
    </div>
    
    <div class="resource-section">
      <h3>资源管理演示</h3>
      <div class="resource-controls">
        <button @click="startTimer">启动定时器</button>
        <button @click="addEventListeners">添加事件监听器</button>
        <button @click="createInterval">创建间隔任务</button>
      </div>
      <div class="resource-status">
        <p>定时器运行: {{ timerRunning ? '是' : '否' }}</p>
        <p>事件监听器添加: {{ eventListenersAdded ? '是' : '否' }}</p>
        <p>间隔任务运行: {{ intervalRunning ? '是' : '否' }}</p>
      </div>
    </div>
    
    <div class="log-section">
      <h3>销毁日志:</h3>
      <ul class="log-list">
        <li v-for="(log, index) in destructionLogs" :key="index">
          {{ log }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, onBeforeUnmount, onUnmounted } from 'vue'
import LifecycleChild from './LifecycleChild.vue'

const showChild = ref(true)
const destructionLogs = ref([])
const timerRunning = ref(false)
const eventListenersAdded = ref(false)
const intervalRunning = ref(false)

let timerId = null
let intervalId = null

// 记录销毁日志
const addDestructionLog = (message) => {
  destructionLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
  console.log(message)
}

// 切换子组件显示
const toggleComponent = () => {
  showChild.value = !showChild.value
}

// 处理子组件事件
const handleChildMounted = () => {
  addDestructionLog('👶 子组件已挂载')
}

const handleChildUnmounted = () => {
  addDestructionLog('👶 子组件已卸载')
}

// 资源管理演示
const startTimer = () => {
  if (timerId) {
    clearTimeout(timerId)
  }
  
  timerRunning.value = true
  timerId = setTimeout(() => {
    addDestructionLog('⏰ 定时器执行完成')
    timerRunning.value = false
  }, 3000)
  
  addDestructionLog('⏰ 定时器已启动 (3秒后执行)')
}

const addEventListeners = () => {
  if (!eventListenersAdded.value) {
    // 添加事件监听器
    const handleClick = () => {
      addDestructionLog('🖱️ 文档点击事件触发')
    }
    
    document.addEventListener('click', handleClick)
    eventListenersAdded.value = true
    addDestructionLog('🖱️ 文档点击事件监听器已添加')
    
    // 保存事件处理函数引用,用于清理
    window._handleClick = handleClick
  }
}

const createInterval = () => {
  if (intervalId) {
    clearInterval(intervalId)
  }
  
  intervalRunning.value = true
  intervalId = setInterval(() => {
    addDestructionLog('⏱️ 间隔任务执行')
  }, 2000)
  
  addDestructionLog('⏱️ 间隔任务已创建 (每2秒执行)')
}

// onBeforeUnmount - 卸载之前
onBeforeUnmount(() => {
  addDestructionLog('🟣 onBeforeUnmount: 组件即将卸载')
  console.log('组件即将被销毁,准备清理资源')
})

// onUnmounted - 卸载之后
onUnmounted(() => {
  addDestructionLog('🔴 onUnmounted: 组件已卸载')
  console.log('组件已被销毁,执行清理工作')
  
  // 清理所有资源
  cleanupResources()
})

// 清理资源函数
const cleanupResources = () => {
  // 清理定时器
  if (timerId) {
    clearTimeout(timerId)
    timerId = null
    timerRunning.value = false
    addDestructionLog('🧹 定时器已清理')
  }
  
  // 清理间隔任务
  if (intervalId) {
    clearInterval(intervalId)
    intervalId = null
    intervalRunning.value = false
    addDestructionLog('🧹 间隔任务已清理')
  }
  
  // 移除事件监听器
  if (eventListenersAdded.value && window._handleClick) {
    document.removeEventListener('click', window._handleClick)
    eventListenersAdded.value = false
    addDestructionLog('🧹 事件监听器已移除')
    delete window._handleClick
  }
}
</script>

<style>
.destruction-stage {
  padding: 20px;
  background-color: #ffebee;
  border-radius: 8px;
  margin: 20px 0;
}

.component-controls {
  margin-bottom: 20px;
}

.toggle-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  background-color: #f44336;
  color: white;
  cursor: pointer;
  font-size: 16px;
}

.toggle-btn:hover {
  background-color: #d32f2f;
}

.child-container {
  margin: 20px 0;
  padding: 20px;
  background-color: #ffcdd2;
  border-radius: 8px;
  border: 2px dashed #f44336;
}

.resource-section {
  margin: 20px 0;
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ffcdd2;
}

.resource-section h3 {
  margin-top: 0;
  color: #495057;
}

.resource-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 15px;
}

.resource-controls button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #e91e63;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

.resource-controls button:hover {
  background-color: #c2185b;
}

.resource-status p {
  margin: 5px 0;
  color: #e91e63;
}

.log-section {
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ffcdd2;
}

.log-list {
  max-height: 300px;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-radius: 4px;
  padding: 10px;
  margin: 0;
  font-size: 14px;
}
</style>
<!-- LifecycleChild.vue -->
<template>
  <div class="lifecycle-child">
    <h3>子组件</h3>
    <p>我是生命周期演示的子组件</p>
    <p>创建时间: {{ creationTime }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const creationTime = ref(new Date().toLocaleTimeString())

// 组件挂载时通知父组件
onMounted(() => {
  console.log('👶 子组件已挂载')
  emit('childMounted')
})

// 组件卸载时通知父组件
onUnmounted(() => {
  console.log('👶 子组件已卸载')
  emit('childUnmounted')
})

const emit = defineEmits(['childMounted', 'childUnmounted'])
</script>

<style scoped>
.lifecycle-child {
  padding: 20px;
  background-color: #fff3e0;
  border-radius: 8px;
  border: 2px solid #ff9800;
  text-align: center;
}

.lifecycle-child h3 {
  margin-top: 0;
  color: #e65100;
}
</style>

实际应用示例

1. 数据获取和网络请求

<template>
  <div class="data-fetching-demo">
    <h2>数据获取生命周期示例</h2>
    
    <div class="controls">
      <button @click="refreshData" :disabled="loading" class="refresh-btn">
        {{ loading ? '加载中...' : '刷新数据' }}
      </button>
      <button @click="clearData" class="clear-btn">清空数据</button>
    </div>
    
    <div class="status-section">
      <p>加载状态: {{ loadingStatus }}</p>
      <p>数据状态: {{ dataStatus }}</p>
      <p>错误信息: {{ errorMessage || '无' }}</p>
    </div>
    
    <div v-if="loading" class="loading-section">
      <div class="spinner"></div>
      <p>正在加载数据...</p>
    </div>
    
    <div v-else-if="userData" class="data-section">
      <h3>用户数据:</h3>
      <div class="user-card">
        <div class="user-avatar">{{ userData.name.charAt(0) }}</div>
        <div class="user-info">
          <p><strong>姓名:</strong> {{ userData.name }}</p>
          <p><strong>邮箱:</strong> {{ userData.email }}</p>
          <p><strong>年龄:</strong> {{ userData.age }}</p>
          <p><strong>城市:</strong> {{ userData.city }}</p>
        </div>
      </div>
    </div>
    
    <div v-else-if="errorMessage" class="error-section">
      <p>❌ {{ errorMessage }}</p>
      <button @click="retryFetch" class="retry-btn">重试</button>
    </div>
    
    <div class="log-section">
      <h3>请求日志:</h3>
      <ul class="log-list">
        <li v-for="(log, index) in fetchLogs" :key="index">
          {{ log }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

const userData = ref(null)
const loading = ref(false)
const loadingStatus = ref('空闲')
const dataStatus = ref('无数据')
const errorMessage = ref('')
const fetchLogs = ref([])

let abortController = null

// 记录请求日志
const addFetchLog = (message) => {
  fetchLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
  console.log(message)
}

// 模拟 API 请求
const fetchUserData = async () => {
  loading.value = true
  loadingStatus.value = '加载中'
  errorMessage.value = ''
  dataStatus.value = '请求中'
  
  addFetchLog('🌐 开始获取用户数据')
  
  // 创建 AbortController 用于取消请求
  abortController = new AbortController()
  
  try {
    // 模拟网络延迟
    await new Promise(resolve => setTimeout(resolve, 2000))
    
    // 模拟随机成功/失败
    if (Math.random() > 0.3) {
      // 模拟成功响应
      const mockData = {
        name: '张三',
        email: 'zhangsan@example.com',
        age: 25,
        city: '北京'
      }
      
      userData.value = mockData
      dataStatus.value = '数据已加载'
      addFetchLog('✅ 用户数据获取成功')
    } else {
      // 模拟失败响应
      throw new Error('网络连接失败,请稍后重试')
    }
  } catch (error) {
    if (error.name !== 'AbortError') {
      errorMessage.value = error.message
      dataStatus.value = '加载失败'
      addFetchLog(`❌ 请求失败: ${error.message}`)
    }
  } finally {
    loading.value = false
    loadingStatus.value = '空闲'
  }
}

// 刷新数据
const refreshData = () => {
  // 取消之前的请求
  if (abortController) {
    abortController.abort()
  }
  fetchUserData()
}

// 重试获取
const retryFetch = () => {
  refreshData()
}

// 清空数据
const clearData = () => {
  userData.value = null
  dataStatus.value = '数据已清空'
  addFetchLog('🧹 数据已清空')
}

// onMounted - 组件挂载后获取数据
onMounted(() => {
  addFetchLog('🟢 onMounted: 组件已挂载,开始获取数据')
  fetchUserData()
})

// onBeforeUnmount - 组件卸载前清理资源
onBeforeUnmount(() => {
  addFetchLog('🟣 onBeforeUnmount: 组件即将卸载,清理资源')
  
  // 取消未完成的请求
  if (abortController) {
    abortController.abort()
    addFetchLog('🧹 未完成的请求已取消')
  }
})
</script>

<style>
.data-fetching-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  background-color: #e8f5e9;
  border-radius: 8px;
}

.controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.refresh-btn, .clear-btn, .retry-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.refresh-btn {
  background-color: #4caf50;
  color: white;
}

.refresh-btn:hover:not(:disabled) {
  background-color: #45a049;
}

.refresh-btn:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.clear-btn, .retry-btn {
  background-color: #ff9800;
  color: white;
}

.clear-btn:hover, .retry-btn:hover {
  background-color: #e68900;
}

.status-section {
  margin: 20px 0;
  padding: 15px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #c8e6c9;
}

.status-section p {
  margin: 8px 0;
  color: #2e7d32;
}

.loading-section {
  text-align: center;
  padding: 40px 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #c8e6c9;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #4caf50;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 20px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.data-section {
  margin: 20px 0;
}

.data-section h3 {
  color: #495057;
  margin-bottom: 15px;
}

.user-card {
  display: flex;
  align-items: center;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  border: 1px solid #c8e6c9;
}

.user-avatar {
  font-size: 48px;
  margin-right: 20px;
}

.user-info p {
  margin: 8px 0;
  color: #495057;
}

.error-section {
  padding: 30px 20px;
  text-align: center;
  background-color: #ffebee;
  border-radius: 4px;
  border: 1px solid #ffcdd2;
  color: #c62828;
}

.log-section {
  margin-top: 30px;
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #c8e6c9;
}

.log-section h3 {
  margin-top: 0;
  color: #495057;
}

.log-list {
  max-height: 200px;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-radius: 4px;
  padding: 10px;
  margin: 0;
  font-size: 14px;
}
</style>

2. 定时器和轮询任务

<template>
  <div class="timer-demo">
    <h2>定时器和轮询任务</h2>
    
    <div class="timer-controls">
      <button @click="startTimer" :disabled="isTimerRunning" class="start-btn">
        开始定时器
      </button>
      <button @click="stopTimer" :disabled="!isTimerRunning" class="stop-btn">
        停止定时器
      </button>
      <button @click="startPolling" :disabled="isPolling" class="poll-start-btn">
        开始轮询
      </button>
      <button @click="stopPolling" :disabled="!isPolling" class="poll-stop-btn">
        停止轮询
      </button>
    </div>
    
    <div class="status-section">
      <div class="status-item">
        <span>定时器状态:</span>
        <span :class="['status', isTimerRunning ? 'running' : 'stopped']">
          {{ isTimerRunning ? '运行中' : '已停止' }}
        </span>
      </div>
      <div class="status-item">
        <span>轮询状态:</span>
        <span :class="['status', isPolling ? 'running' : 'stopped']">
          {{ isPolling ? '运行中' : '已停止' }}
        </span>
      </div>
      <div class="status-item">
        <span>定时器计数:</span>
        <span class="count">{{ timerCount }}</span>
      </div>
      <div class="status-item">
        <span>轮询计数:</span>
        <span class="count">{{ pollingCount }}</span>
      </div>
    </div>
    
    <div class="clock-section">
      <h3>实时时钟</h3>
      <div class="clock-display">
        {{ currentTime }}
      </div>
    </div>
    
    <div class="log-section">
      <h3>定时器日志:</h3>
      <ul class="log-list">
        <li v-for="(log, index) in timerLogs" :key="index">
          {{ log }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

const isTimerRunning = ref(false)
const isPolling = ref(false)
const timerCount = ref(0)
const pollingCount = ref(0)
const currentTime = ref(new Date().toLocaleTimeString())
const timerLogs = ref([])

let timerId = null
let pollingInterval = null
let clockInterval = null

// 记录定时器日志
const addTimerLog = (message) => {
  timerLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
  console.log(message)
}

// 定时器任务
const startTimer = () => {
  if (isTimerRunning.value) return
  
  isTimerRunning.value = true
  timerCount.value = 0
  
  timerId = setTimeout(() => {
    addTimerLog('⏰ 3秒定时器执行')
    isTimerRunning.value = false
  }, 3000)
  
  addTimerLog('⏰ 定时器已启动 (3秒后执行)')
}

const stopTimer = () => {
  if (timerId) {
    clearTimeout(timerId)
    timerId = null
    isTimerRunning.value = false
    addTimerLog('🧹 定时器已停止')
  }
}

// 轮询任务
const startPolling = () => {
  if (isPolling.value) return
  
  isPolling.value = true
  pollingCount.value = 0
  
  pollingInterval = setInterval(() => {
    pollingCount.value++
    addTimerLog(`⏱️ 轮询任务执行 #${pollingCount.value}`)
  }, 2000)
  
  addTimerLog('⏱️ 轮询已启动 (每2秒执行)')
}

const stopPolling = () => {
  if (pollingInterval) {
    clearInterval(pollingInterval)
    pollingInterval = null
    isPolling.value = false
    addTimerLog('🧹 轮询已停止')
  }
}

// 实时时钟
const updateClock = () => {
  currentTime.value = new Date().toLocaleTimeString()
}

// onMounted - 组件挂载后启动时钟
onMounted(() => {
  addTimerLog('🟢 onMounted: 组件已挂载')
  
  // 启动实时时钟
  clockInterval = setInterval(updateClock, 1000)
  addTimerLog('🕒 实时时钟已启动')
  
  // 可以在这里启动默认的定时器或轮询
  // startTimer()
  // startPolling()
})

// onBeforeUnmount - 组件卸载前清理所有定时器
onBeforeUnmount(() => {
  addTimerLog('🟣 onBeforeUnmount: 组件即将卸载')
  
  // 清理所有定时器和间隔任务
  stopTimer()
  stopPolling()
  
  if (clockInterval) {
    clearInterval(clockInterval)
    clockInterval = null
    addTimerLog('🧹 实时时钟已停止')
  }
  
  addTimerLog('🧹 所有定时器资源已清理')
})
</script>

<style>
.timer-demo {
  max-width: 700px;
  margin: 0 auto;
  padding: 20px;
  background-color: #fff3e0;
  border-radius: 8px;
}

.timer-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 30px;
}

.start-btn, .stop-btn, .poll-start-btn, .poll-stop-btn {
  padding: 10px 16px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.start-btn {
  background-color: #4caf50;
  color: white;
}

.start-btn:hover:not(:disabled) {
  background-color: #45a049;
}

.stop-btn {
  background-color: #f44336;
  color: white;
}

.stop-btn:hover:not(:disabled) {
  background-color: #da190b;
}

.poll-start-btn {
  background-color: #2196f3;
  color: white;
}

.poll-start-btn:hover:not(:disabled) {
  background-color: #0b7dda;
}

.poll-stop-btn {
  background-color: #ff9800;
  color: white;
}

.poll-stop-btn:hover:not(:disabled) {
  background-color: #e68900;
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.status-section {
  margin: 25px 0;
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ffe0b2;
}

.status-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #eee;
}

.status-item:last-child {
  border-bottom: none;
}

.status {
  padding: 4px 12px;
  border-radius: 12px;
  font-size: 14px;
  font-weight: bold;
}

.status.running {
  background-color: #c8e6c9;
  color: #2e7d32;
}

.status.stopped {
  background-color: #ffcdd2;
  color: #c62828;
}

.count {
  font-weight: bold;
  color: #ff9800;
}

.clock-section {
  text-align: center;
  margin: 30px 0;
}

.clock-section h3 {
  color: #495057;
  margin-bottom: 15px;
}

.clock-display {
  font-size: 36px;
  font-weight: bold;
  font-family: 'Courier New', monospace;
  color: #e65100;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  border: 2px solid #ff9800;
}

.log-section {
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ffe0b2;
}

.log-section h3 {
  margin-top: 0;
  color: #495057;
}

.log-list {
  max-height: 300px;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-radius: 4px;
  padding: 10px;
  margin: 0;
  font-size: 14px;
}
</style>

3. 事件监听器管理

<template>
  <div class="event-demo">
    <h2>事件监听器管理</h2>
    
    <div class="event-controls">
      <button @click="addEventListeners" :disabled="listenersAdded" class="add-btn">
        添加事件监听器
      </button>
      <button @click="removeEventListeners" :disabled="!listenersAdded" class="remove-btn">
        移除事件监听器
      </button>
      <button @click="toggleWindowEvents" class="toggle-btn">
        {{ windowEventsEnabled ? '禁用窗口事件' : '启用窗口事件' }}
      </button>
    </div>
    
    <div class="status-section">
      <div class="status-grid">
        <div class="status-item">
          <span>监听器状态:</span>
          <span :class="['status', listenersAdded ? 'active' : 'inactive']">
            {{ listenersAdded ? '已添加' : '未添加' }}
          </span>
        </div>
        <div class="status-item">
          <span>窗口事件:</span>
          <span :class="['status', windowEventsEnabled ? 'active' : 'inactive']">
            {{ windowEventsEnabled ? '已启用' : '已禁用' }}
          </span>
        </div>
        <div class="status-item">
          <span>点击计数:</span>
          <span class="count">{{ clickCount }}</span>
        </div>
        <div class="status-item">
          <span>键盘计数:</span>
          <span class="count">{{ keyCount }}</span>
        </div>
      </div>
    </div>
    
    <div class="interaction-area">
      <h3>交互区域</h3>
      <div 
        class="click-area"
        @click="handleAreaClick"
        :style="{ backgroundColor: areaColor }"
      >
        <p>点击这个区域</p>
        <p>当前颜色: {{ areaColor }}</p>
      </div>
      
      <div class="keyboard-area">
        <p>在页面任意位置按键盘:</p>
        <p>最后按键: {{ lastKey }}</p>
        <p>按 Ctrl+C 复制文本</p>
      </div>
    </div>
    
    <div class="log-section">
      <h3>事件日志:</h3>
      <ul class="log-list">
        <li v-for="(log, index) in eventLogs" :key="index">
          {{ log }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

const listenersAdded = ref(false)
const windowEventsEnabled = ref(false)
const clickCount = ref(0)
const keyCount = ref(0)
const lastKey = ref('')
const areaColor = ref('#e3f2fd')
const eventLogs = ref([])

// 事件处理函数(需要保存引用以便移除)
const handleClick = (event) => {
  clickCount.value++
  addEventLog(`🖱️ 页面点击事件: (${event.clientX}, ${event.clientY})`)
}

const handleKeydown = (event) => {
  keyCount.value++
  lastKey.value = event.key
  addEventLog(`⌨️ 键盘按下: ${event.key} (${event.code})`)
  
  // 特殊按键处理
  if (event.ctrlKey && event.key === 'c') {
    addEventLog('📋 Ctrl+C 被按下')
  }
}

const handleResize = () => {
  addEventLog(`📐 窗口大小改变: ${window.innerWidth} x ${window.innerHeight}`)
}

const handleScroll = () => {
  addEventLog(`📜 页面滚动: ${window.scrollY}px`)
}

const handleMouseMove = (event) => {
  // 限制日志频率
  if (eventLogs.value.length % 10 === 0) {
    addEventLog(`🖱️ 鼠标移动: (${event.clientX}, ${event.clientY})`)
  }
}

// 记录事件日志
const addEventLog = (message) => {
  eventLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
  console.log(message)
  
  // 限制日志数量
  if (eventLogs.value.length > 100) {
    eventLogs.value.shift()
  }
}

// 添加事件监听器
const addEventListeners = () => {
  if (listenersAdded.value) return
  
  // 添加各种事件监听器
  document.addEventListener('click', handleClick)
  document.addEventListener('keydown', handleKeydown)
  window.addEventListener('resize', handleResize)
  window.addEventListener('scroll', handleScroll)
  document.addEventListener('mousemove', handleMouseMove)
  
  listenersAdded.value = true
  addEventLog('👂 事件监听器已添加')
}

// 移除事件监听器
const removeEventListeners = () => {
  if (!listenersAdded.value) return
  
  // 移除事件监听器
  document.removeEventListener('click', handleClick)
  document.removeEventListener('keydown', handleKeydown)
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('scroll', handleScroll)
  document.removeEventListener('mousemove', handleMouseMove)
  
  listenersAdded.value = false
  addEventLog('🧹 事件监听器已移除')
}

// 切换窗口事件
const toggleWindowEvents = () => {
  windowEventsEnabled.value = !windowEventsEnabled.value
  
  if (windowEventsEnabled.value) {
    window.addEventListener('resize', handleResize)
    window.addEventListener('scroll', handleScroll)
    addEventLog('👂 窗口事件监听器已启用')
  } else {
    window.removeEventListener('resize', handleResize)
    window.removeEventListener('scroll', handleScroll)
    addEventLog('🧹 窗口事件监听器已禁用')
  }
}

// 区域点击处理
const handleAreaClick = () => {
  // 改变区域颜色
  const colors = ['#e3f2fd', '#f3e5f5', '#e8f5e9', '#fff3e0', '#fce4ec']
  const currentIndex = colors.indexOf(areaColor.value)
  const nextIndex = (currentIndex + 1) % colors.length
  areaColor.value = colors[nextIndex]
  
  addEventLog('🎨 区域点击,颜色已改变')
}

// onMounted - 组件挂载后添加默认监听器
onMounted(() => {
  addEventLog('🟢 onMounted: 组件已挂载')
  
  // 可以在这里添加默认的事件监听器
  // addEventListeners()
})

// onBeforeUnmount - 组件卸载前移除所有监听器
onBeforeUnmount(() => {
  addEventLog('🟣 onBeforeUnmount: 组件即将卸载')
  
  // 确保移除所有事件监听器
  removeEventListeners()
  toggleWindowEvents()
  
  addEventLog('🧹 所有事件监听器已清理')
})
</script>

<style>
.event-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  background-color: #f3e5f5;
  border-radius: 8px;
}

.event-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 30px;
}

.add-btn, .remove-btn, .toggle-btn {
  padding: 10px 16px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.add-btn {
  background-color: #9c27b0;
  color: white;
}

.add-btn:hover:not(:disabled) {
  background-color: #7b1fa2;
}

.remove-btn {
  background-color: #f44336;
  color: white;
}

.remove-btn:hover:not(:disabled) {
  background-color: #da190b;
}

.toggle-btn {
  background-color: #ff9800;
  color: white;
}

.toggle-btn:hover {
  background-color: #e68900;
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.status-section {
  margin: 25px 0;
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 15px;
}

.status-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ce93d8;
}

.status {
  padding: 4px 12px;
  border-radius: 12px;
  font-size: 14px;
  font-weight: bold;
}

.status.active {
  background-color: #e8f5e9;
  color: #2e7d32;
}

.status.inactive {
  background-color: #ffcdd2;
  color: #c62828;
}

.count {
  font-weight: bold;
  color: #9c27b0;
}

.interaction-area {
  margin: 30px 0;
}

.interaction-area h3 {
  color: #495057;
  margin-bottom: 20px;
}

.click-area {
  padding: 40px 20px;
  text-align: center;
  border-radius: 8px;
  margin-bottom: 30px;
  cursor: pointer;
  transition: all 0.3s ease;
  border: 2px dashed #9c27b0;
}

.click-area:hover {
  transform: scale(1.02);
}

.click-area p {
  margin: 10px 0;
  color: #495057;
  font-weight: bold;
}

.keyboard-area {
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  border: 1px solid #ce93d8;
  text-align: center;
}

.keyboard-area p {
  margin: 10px 0;
  color: #495057;
}

.log-section {
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #ce93d8;
}

.log-section h3 {
  margin-top: 0;
  color: #495057;
}

.log-list {
  max-height: 300px;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-radius: 4px;
  padding: 10px;
  margin: 0;
  font-size: 14px;
}
</style>

生命周期钩子对比表

Vue2 vs Vue3

Vue2 选项式 APIVue3 组合式 API说明
beforeCreatesetup()组件创建前
createdsetup()组件创建后
beforeMountonBeforeMount挂载前
mountedonMounted挂载后
beforeUpdateonBeforeUpdate更新前
updatedonUpdated更新后
beforeDestroyonBeforeUnmount销毁前
destroyedonUnmounted销毁后

Vue3 新增的生命周期钩子

<template>
  <div class="new-lifecycle-demo">
    <h2>Vue3 新增生命周期钩子</h2>
    
    <div class="render-demo">
      <h3>渲染跟踪演示</h3>
      <button @click="updateData">更新数据</button>
      <p>计数器: {{ counter }}</p>
      <p>双倍: {{ doubleCounter }}</p>
    </div>
    
    <div class="error-demo">
      <h3>错误处理演示</h3>
      <button @click="triggerError">触发错误</button>
      <button @click="clearError">清除错误</button>
      <p v-if="error" class="error-message">错误: {{ error.message }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onErrorCaptured, onRenderTracked, onRenderTriggered } from 'vue'

const counter = ref(0)
const error = ref(null)

// 计算属性
const doubleCounter = computed(() => counter.value * 2)

// 更新数据
const updateData = () => {
  counter.value++
}

// 触发错误
const triggerError = () => {
  throw new Error('这是一个测试错误')
}

// 清除错误
const clearError = () => {
  error.value = null
}

// onRenderTracked - 响应式依赖被追踪时触发
onRenderTracked((event) => {
  console.log('🔍 onRenderTracked:', event)
  // event 包含 type, key, target, effect 等信息
})

// onRenderTriggered - 响应式依赖被触发时触发
onRenderTriggered((event) => {
  console.log('⚡ onRenderTriggered:', event)
  // 当响应式数据变化导致重新渲染时触发
})

// onErrorCaptured - 捕获子组件错误
onErrorCaptured((err, instance, info) => {
  console.log('💥 onErrorCaptured:', err, instance, info)
  error.value = err
  return false // 阻止错误继续传播
})
</script>

<style>
.new-lifecycle-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  background-color: #e1f5fe;
  border-radius: 8px;
}

.render-demo, .error-demo {
  margin: 25px 0;
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #b3e5fc;
}

.render-demo h3, .error-demo h3 {
  margin-top: 0;
  color: #495057;
}

button {
  padding: 8px 16px;
  margin: 5px;
  border: none;
  border-radius: 4px;
  background-color: #0277bd;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

button:hover {
  background-color: #01579b;
}

.error-message {
  color: #c62828;
  background-color: #ffebee;
  padding: 10px;
  border-radius: 4px;
  margin-top: 15px;
}
</style>

总结

生命周期钩子使用场景

钩子使用场景
onMounted发起网络请求、访问 DOM、启动定时器
onBeforeUpdate在 DOM 更新前获取当前状态
onUpdatedDOM 更新后执行操作(谨慎使用)
onBeforeUnmount清理定时器、事件监听器、取消网络请求
onUnmounted最后的清理工作

最佳实践

  1. 资源管理

    • onBeforeUnmount 中清理定时器和事件监听器
    • 使用 AbortController 取消网络请求
  2. 性能优化

    • 避免在 onUpdated 中修改响应式数据
    • 合理使用防抖和节流
  3. 错误处理

    • 使用 onErrorCaptured 捕获子组件错误
    • 提供友好的错误提示

记忆口诀

  • 创建阶段:setup → onBeforeMount → onMounted
  • 更新阶段:onBeforeUpdate → onUpdated
  • 销毁阶段:onBeforeUnmount → onUnmounted
  • 挂载后:可以访问 DOM 和发起请求
  • 销毁前:记得清理资源和定时器

这样就能很好地掌握 Vue3 组件的生命周期了!