Vue3 组件生命周期详解

Vue3 组件生命周期详解

核心概念理解

什么是生命周期?

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

为什么要学习生命周期?

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

Vue3 生命周期钩子

1. 创建阶段

vue 复制代码
<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)

vue 复制代码
<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)

vue 复制代码
<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)

vue 复制代码
<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>
vue 复制代码
<!-- 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. 数据获取和网络请求

vue 复制代码
<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. 定时器和轮询任务

vue 复制代码
<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. 事件监听器管理

vue 复制代码
<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 选项式 API Vue3 组合式 API 说明
beforeCreate setup() 组件创建前
created setup() 组件创建后
beforeMount onBeforeMount 挂载前
mounted onMounted 挂载后
beforeUpdate onBeforeUpdate 更新前
updated onUpdated 更新后
beforeDestroy onBeforeUnmount 销毁前
destroyed onUnmounted 销毁后

Vue3 新增的生命周期钩子

vue 复制代码
<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 更新前获取当前状态
onUpdated DOM 更新后执行操作(谨慎使用)
onBeforeUnmount 清理定时器、事件监听器、取消网络请求
onUnmounted 最后的清理工作

最佳实践

  1. 资源管理

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

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

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

记忆口诀

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

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

相关推荐
布列瑟农的星空13 分钟前
大话设计模式——关注点分离原则下的事件处理
前端·后端·架构
yvvvy32 分钟前
前端必懂的 Cache 缓存机制详解
前端
北海几经夏1 小时前
React自定义Hook
前端·react.js
龙在天1 小时前
从代码到屏幕,浏览器渲染网页做了什么❓
前端
TimelessHaze1 小时前
【performance面试考点】让面试官眼前一亮的performance性能优化
前端·性能优化·trae
yes or ok1 小时前
前端工程师面试题-vue
前端·javascript·vue.js
我要成为前端高手1 小时前
给不支持摇树的三方库(phaser) tree-shake?
前端·javascript
Noxi_lumors2 小时前
VITE BALABALA require balabla not supported
前端·vite
周胜22 小时前
node-sass
前端
aloha_2 小时前
Windows 系统中,杀死占用某个端口(如 8080)的进程
前端