【Vue 3 系列·第四篇】生命周期钩子完全指南:执行时机、Vue 2 对比、Composable 中的正确用法

更新时间:2026-05-19 | 阅读时长:约 20 分钟
系列:Vue 3 完全指南(共 8 篇)
标签Vue3 生命周期 onMounted onUnmounted onBeforeUpdate onUpdated Composable Vue2对比


在这里插入图片描述

系列进度

篇次 主题 状态
第一篇 Vue 3 是什么:Composition API vs Options API ✅ 已发布
第二篇 响应式系统:ref、reactive、computed、watch ✅ 已发布
第三篇 组件通信:props、emit、provide/inject ✅ 已发布
第四篇(本篇) 生命周期钩子:执行时机与正确用法
第五篇 路由:Vue Router 4 实战 即将发布
第六篇 状态管理:Pinia 完全指南 即将发布
第七篇 性能优化:懒加载、keep-alive、v-memo 即将发布
第八篇 工程化:Vite + TypeScript + 最佳实践 即将发布

目录


一、生命周期全景图

组件生命周期(Vue 3 Composition API):

┌─────────────────────────────────────────────┐
│  createApp() / 父组件渲染到该组件            │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  setup()                                    │
│  ← beforeCreate + created 的替代            │
│  执行响应式数据初始化、computed、watch 等    │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  onBeforeMount()                            │
│  ← 模板编译完成,DOM 尚未插入               │
│  此时 $el 还不存在                           │
└──────────────────┬──────────────────────────┘
                   │
                   ▼  DOM 渲染
┌─────────────────────────────────────────────┐
│  onMounted()              ← 最常用 ★        │
│  DOM 已插入,可以访问 DOM、初始化第三方库    │
│  可以发起数据请求                            │
└──────────────────┬──────────────────────────┘
                   │
                   │  数据变化触发重渲染(循环)
                   ▼
┌─────────────────────────────────────────────┐
│  onBeforeUpdate()                           │
│  ← 数据已变,DOM 尚未更新                   │
│  可以在此访问更新前的 DOM 状态              │
└──────────────────┬──────────────────────────┘
                   │
                   ▼  DOM 更新
┌─────────────────────────────────────────────┐
│  onUpdated()                                │
│  DOM 已更新,可以访问最新 DOM               │
│  ⚠️ 避免在此修改状态(会导致无限循环)      │
└──────────────────┬──────────────────────────┘
                   │
                   │  组件被移除
                   ▼
┌─────────────────────────────────────────────┐
│  onBeforeUnmount()                          │
│  ← 组件仍然完好,即将销毁                   │
│  最后一次访问 DOM 的机会                    │
└──────────────────┬──────────────────────────┘
                   │
                   ▼  清理
┌─────────────────────────────────────────────┐
│  onUnmounted()            ← 最常用 ★        │
│  组件已销毁                                  │
│  清理定时器、取消订阅、移除事件监听          │
└─────────────────────────────────────────────┘

二、Vue 2 vs Vue 3 生命周期对比

Vue 2 选项 Vue 3 钩子函数 说明
beforeCreate ❌ 无对应(用 setup() 替代) setup 在其之前执行
created ❌ 无对应(用 setup() 替代) setup 包含了这两者
beforeMount onBeforeMount 挂载前
mounted onMounted 挂载完成
beforeUpdate onBeforeUpdate 更新前
updated onUpdated 更新完成
beforeDestroy onBeforeUnmount 卸载前(改名了!)
destroyed onUnmounted 卸载完成(改名了!)
activated onActivated keep-alive 激活
deactivated onDeactivated keep-alive 停用
errorCaptured onErrorCaptured 捕获子组件错误
onRenderTracked 调试:追踪响应式依赖(开发环境)
onRenderTriggered 调试:触发重渲染(开发环境)
onServerPrefetch SSR 专用
关键变化记忆:
beforeDestroy  → onBeforeUnmount   (destroy 改为 unmount)
destroyed      → onUnmounted       (destroy 改为 unmount)
beforeCreate + created → setup()   (合并了!)

三、创建阶段:setup 就是新的 beforeCreate + created

<script setup lang="ts">
import { ref, computed, watch } from 'vue'

// setup() 在组件实例创建时同步执行
// 等价于 Vue 2 的 beforeCreate + created

// ✅ 可以做:
// 初始化响应式数据
const count = ref(0)
const name  = ref('Vue')

// 定义计算属性
const doubled = computed(() => count.value * 2)

// 注册侦听器
watch(count, (val) => console.log('count changed:', val))

// 同步的初始化逻辑
const initialData = processData()

// ⚠️ 不能做:
// 访问 DOM(DOM 还没有渲染)
// document.querySelector('.my-el')  // ❌ 元素不存在

// 访问子组件实例
// childRef.value?.someMethod()      // ❌ 子组件还没挂载

// 注意:setup 中的 console.log 会在组件渲染前执行
console.log('setup 执行,此时 DOM 还不存在')

function processData() {
  return { items: [], total: 0 }
}
</script>

3.1 在 setup 中发起异步请求

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const users = ref([])
const loading = ref(false)

// ❌ 不推荐:直接在 setup 顶层 await
// 这需要组件使用 <Suspense> 包裹,且行为特殊
// const data = await fetchUsers()

// ✅ 推荐:在 onMounted 中发起请求(或使用 watch)
onMounted(async () => {
  loading.value = true
  try {
    users.value = await fetchUsers()
  } finally {
    loading.value = false
  }
})

// ✅ 也可以:定义异步函数,在 setup 中同步调用(不 await)
async function loadUsers() {
  loading.value = true
  try {
    users.value = await fetchUsers()
  } finally {
    loading.value = false
  }
}

// 立即执行(不阻塞 setup)
loadUsers()

async function fetchUsers() {
  const res = await fetch('/api/users')
  return res.json()
}
</script>

四、挂载阶段:onBeforeMount 与 onMounted

4.1 onBeforeMount

<script setup lang="ts">
import { onBeforeMount } from 'vue'

onBeforeMount(() => {
  // 此时:
  // ✅ 响应式数据已准备好
  // ✅ 模板已编译成渲染函数
  // ❌ DOM 还未插入页面
  // ❌ $el 还不存在

  console.log('onBeforeMount:即将挂载到 DOM')

  // 使用场景:非常少见
  // 可以做最后一次数据初始化(但通常在 setup 就做了)
})
</script>

4.2 onMounted(最常用)

<script setup lang="ts">
import { ref, onMounted, useTemplateRef } from 'vue'

const chartRef = useTemplateRef<HTMLDivElement>('chartRef')
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')

onMounted(() => {
  // 此时:
  // ✅ DOM 已插入页面
  // ✅ 可以访问 DOM 元素
  // ✅ 子组件也已挂载完成
  // ✅ 可以初始化需要 DOM 的第三方库

  console.log('onMounted:DOM 已就绪')
  console.log(chartRef.value)   // HTMLDivElement

  // 常见用途1:初始化 ECharts / D3.js 等图表库
  const chart = echarts.init(chartRef.value!)
  chart.setOption({ /* ... */ })

  // 常见用途2:自动聚焦
  inputRef.value?.focus()

  // 常见用途3:设置事件监听(记得在 onUnmounted 中移除!)
  window.addEventListener('resize', handleResize)

  // 常见用途4:发起数据请求
  fetchInitialData()
})

function handleResize() { /* ... */ }
async function fetchInitialData() { /* ... */ }
</script>

<template>
  <div ref="chartRef" class="chart-container" />
  <input ref="inputRef" />
</template>

4.3 onMounted 的执行顺序(父子组件)

父组件 onBeforeMount
  子组件 onBeforeMount
  子组件 onMounted       ← 子先挂载完
父组件 onMounted         ← 父后挂载完

规律:子组件先 mounted,父组件后 mounted
这意味着:父组件的 onMounted 中可以安全访问子组件实例
<!-- 父组件 -->
<script setup>
import { onMounted, useTemplateRef } from 'vue'
import ChildComponent from './ChildComponent.vue'

const childRef = useTemplateRef('childRef')

onMounted(() => {
  // ✅ 此时子组件已经挂载完成,可以调用其方法
  childRef.value?.initialize()
})
</script>

<template>
  <ChildComponent ref="childRef" />
</template>

五、更新阶段:onBeforeUpdate 与 onUpdated

<script setup lang="ts">
import { ref, onBeforeUpdate, onUpdated, useTemplateRef } from 'vue'

const count = ref(0)
const listRef = useTemplateRef<HTMLUListElement>('listRef')

onBeforeUpdate(() => {
  // 数据已变,DOM 还未更新
  // 可以访问更新前的 DOM 状态
  console.log('DOM 更新前,列表高度:', listRef.value?.offsetHeight)
  // 使用场景:保存更新前的 DOM 状态(如滚动位置)
})

onUpdated(() => {
  // DOM 已更新,可以访问最新 DOM
  console.log('DOM 已更新,列表高度:', listRef.value?.offsetHeight)

  // ⚠️ 重要警告:不要在 onUpdated 中修改响应式数据!
  // count.value++  // ❌ 会导致无限更新循环!

  // ✅ 如果需要在更新后做某事,用条件限制
  // if (someCondition) { ... }  // 确保不会每次更新都触发修改

  // ✅ 正确使用:读取更新后的 DOM 信息
  // 比如滚动到新增的消息(聊天应用)
})
</script>

<template>
  <button @click="count++">+1 ({{ count }})</button>
  <ul ref="listRef">
    <li v-for="i in count" :key="i">Item {{ i }}</li>
  </ul>
</template>

5.1 onUpdated vs watch + nextTick

<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'

const message = ref('')

// 方式1:watch + nextTick(更精确,只在指定数据变化时执行)
watch(message, async () => {
  await nextTick()  // 等待 DOM 更新完成
  // 此时 DOM 已更新,与 onUpdated 类似
  console.log('message 变化后的 DOM:', document.getElementById('msg')?.textContent)
})

// 方式2:onUpdated(任意数据变化都触发,粒度粗)
// 通常 watch + nextTick 比 onUpdated 更精确
</script>

六、卸载阶段:onBeforeUnmount 与 onUnmounted

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onUnmounted } from 'vue'

const timer     = ref<ReturnType<typeof setInterval>>()
const wsClient  = ref<WebSocket>()
const observer  = ref<IntersectionObserver>()

onMounted(() => {
  // 设置各种需要清理的资源

  // 1. 定时器
  timer.value = setInterval(() => {
    console.log('timer tick')
  }, 1000)

  // 2. WebSocket
  wsClient.value = new WebSocket('ws://example.com')
  wsClient.value.onmessage = (e) => console.log(e.data)

  // 3. 观察器
  observer.value = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) loadMoreData()
    })
  })
  observer.value.observe(document.querySelector('.sentinel')!)

  // 4. 全局事件监听
  window.addEventListener('resize', handleResize)
  document.addEventListener('keydown', handleKeydown)
})

onBeforeUnmount(() => {
  // 组件即将销毁,DOM 还在
  // 最后一次读取 DOM 信息的机会
  console.log('即将销毁,最后的 DOM 访问')
})

onUnmounted(() => {
  // 组件已销毁,在这里清理所有副作用

  // 1. 清除定时器
  if (timer.value) clearInterval(timer.value)

  // 2. 关闭 WebSocket
  wsClient.value?.close()

  // 3. 断开观察器
  observer.value?.disconnect()

  // 4. 移除全局事件监听
  window.removeEventListener('resize', handleResize)
  document.removeEventListener('keydown', handleKeydown)

  console.log('已清理所有副作用')
})

function handleResize() { /* ... */ }
function handleKeydown(e: KeyboardEvent) { /* ... */ }
function loadMoreData() { /* ... */ }
</script>

6.1 自动清理(推荐模式)

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'

// 把"注册 + 清理"封装在一起,不容易遗漏
function useEventListener(
  target: EventTarget,
  event: string,
  handler: EventListener,
) {
  onMounted(() => target.addEventListener(event, handler))
  onUnmounted(() => target.removeEventListener(event, handler))
}

// 使用:不需要手动在 onUnmounted 里清理
useEventListener(window, 'resize', () => console.log('resized'))
useEventListener(document, 'keydown', (e) => console.log(e))

// watchEffect 也会自动在组件卸载时停止
// watch 也是(在 setup 顶层创建时)
</script>

七、特殊钩子

7.1 keep-alive 相关:onActivated / onDeactivated

<!-- 父组件:使用 keep-alive 缓存组件 -->
<template>
  <keep-alive>
    <component :is="currentView" />
  </keep-alive>
</template>
<!-- 被缓存的子组件 -->
<script setup lang="ts">
import { onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'

onMounted(() => {
  // 第一次挂载时执行(只执行一次)
  console.log('首次挂载')
})

onUnmounted(() => {
  // keep-alive 包裹时:不会执行!(组件被缓存,不是销毁)
  // 只有手动销毁 keep-alive 时才执行
  console.log('真正销毁')
})

onActivated(() => {
  // 每次组件从缓存中激活时执行(包括第一次)
  console.log('激活(从缓存恢复)')
  // 使用场景:刷新数据(如页面从 tab 切换回来)
  refreshData()
})

onDeactivated(() => {
  // 每次组件被缓存(不是销毁)时执行
  console.log('停用(进入缓存)')
  // 使用场景:暂停视频、停止动画等
  pauseVideo()
})

function refreshData() { /* 重新拉取数据 */ }
function pauseVideo() { /* 暂停播放 */ }
</script>

7.2 错误捕获:onErrorCaptured

<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue'

const hasError = ref(false)
const errorMessage = ref('')

// 捕获后代组件抛出的错误
onErrorCaptured((error, instance, info) => {
  hasError.value = true
  errorMessage.value = error.message

  console.error('捕获到错误:', error)
  console.error('来自组件:', instance)
  console.error('错误信息:', info)  // 如 'in mounted hook'

  // 返回 false:阻止错误继续向上传播
  return false
  // 返回 true 或不返回:错误继续向上传播
})
</script>

<template>
  <div v-if="hasError" class="error-boundary">
    <p>出错了:{{ errorMessage }}</p>
    <button @click="hasError = false">重试</button>
  </div>
  <slot v-else />
</template>

7.3 调试钩子(开发环境)

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

// 仅在开发环境有效,生产环境不执行

// 每次组件渲染时追踪哪些响应式依赖被访问
onRenderTracked((event) => {
  console.log('依赖被追踪:', event)
  // event.key:被访问的属性名
  // event.type:操作类型(get/has/iterate)
})

// 每次组件重新渲染时,追踪是哪个依赖变化触发的
onRenderTriggered((event) => {
  console.log('重渲染触发原因:', event)
  // event.key:变化的属性名
  // event.newValue:新值
  // event.oldValue:旧值
})
</script>

八、在 Composable 中使用生命周期

这是 Composition API 的核心优势之一:生命周期钩子可以在 Composable 中使用,让逻辑更内聚。

8.1 基本模式

// composables/useWindowSize.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
  const width  = ref(window.innerWidth)
  const height = ref(window.innerHeight)

  function update() {
    width.value  = window.innerWidth
    height.value = window.innerHeight
  }

  // 生命周期钩子在 Composable 中完全有效!
  // 它们会绑定到调用该 Composable 的组件实例
  onMounted(() => {
    window.addEventListener('resize', update)
  })

  onUnmounted(() => {
    window.removeEventListener('resize', update)
  })

  return { width, height }
}
<!-- 组件中使用 -->
<script setup>
import { useWindowSize } from '@/composables/useWindowSize'

// 自动处理事件监听的注册和清理,组件无需关心
const { width, height } = useWindowSize()
</script>

<template>
  <p>窗口大小:{{ width }} × {{ height }}</p>
</template>

8.2 注意:Composable 必须在 setup 中同步调用

<script setup>
import { ref, onMounted } from 'vue'
import { useWindowSize } from '@/composables/useWindowSize'

// ✅ 在 setup 顶层同步调用
const { width } = useWindowSize()

// ❌ 不能在条件语句中调用
if (someCondition) {
  const { width } = useWindowSize()  // ❌ 生命周期钩子无法绑定到组件
}

// ❌ 不能在异步之后调用
onMounted(async () => {
  await someAsyncOperation()
  const { width } = useWindowSize()  // ❌ 此时 setup 已执行完,无法绑定
})

// ✅ 如果需要条件逻辑,在 Composable 内部处理
</script>

8.3 常用 Composable 示例

// composables/useInterval.ts
// 封装定时器,自动在组件卸载时清理
import { onMounted, onUnmounted } from 'vue'

export function useInterval(fn: () => void, delay: number) {
  let timer: ReturnType<typeof setInterval>

  onMounted(() => {
    timer = setInterval(fn, delay)
  })

  onUnmounted(() => {
    clearInterval(timer)
  })
}
// composables/useIntersectionObserver.ts
// 封装 IntersectionObserver,用于懒加载
import { ref, onMounted, onUnmounted } from 'vue'
import type { Ref } from 'vue'

export function useIntersectionObserver(
  target: Ref<Element | null | undefined>,
  callback: IntersectionObserverCallback,
  options?: IntersectionObserverInit,
) {
  const isIntersecting = ref(false)
  let observer: IntersectionObserver

  onMounted(() => {
    if (!target.value) return

    observer = new IntersectionObserver((entries, obs) => {
      isIntersecting.value = entries[0]?.isIntersecting ?? false
      callback(entries, obs)
    }, options)

    observer.observe(target.value)
  })

  onUnmounted(() => {
    observer?.disconnect()
  })

  return { isIntersecting }
}
// composables/usePageVisibility.ts
// 页面可见性变化(如切换浏览器 Tab)
import { ref, onMounted, onUnmounted } from 'vue'

export function usePageVisibility() {
  const isVisible = ref(!document.hidden)

  function handleVisibilityChange() {
    isVisible.value = !document.hidden
  }

  onMounted(() => {
    document.addEventListener('visibilitychange', handleVisibilityChange)
  })

  onUnmounted(() => {
    document.removeEventListener('visibilitychange', handleVisibilityChange)
  })

  return { isVisible }
}

九、完整实战:数据大屏组件

综合运用各生命周期钩子,实现一个实时数据大屏:

<!-- DataDashboard.vue -->
<script setup lang="ts">
import {
  ref, reactive, computed,
  onMounted, onBeforeUnmount, onUnmounted,
  onActivated, onDeactivated,
  onErrorCaptured,
} from 'vue'
import { useWindowSize } from '@/composables/useWindowSize'
import { useInterval } from '@/composables/useInterval'

// ── 窗口尺寸(Composable 自动处理监听和清理)──────────────
const { width, height } = useWindowSize()

// ── 响应式数据 ────────────────────────────────────────────
const metrics = reactive({
  visitors: 0,
  orders:   0,
  revenue:  0,
  errors:   0,
})

const chartInstance   = ref<any>(null)    // ECharts 实例
const wsConnection    = ref<WebSocket>()
const isConnected     = ref(false)
const hasError        = ref(false)
const errorMsg        = ref('')
const lastUpdateTime  = ref<Date>()

// ── 计算属性 ──────────────────────────────────────────────
const conversionRate = computed(() =>
  metrics.visitors > 0
    ? ((metrics.orders / metrics.visitors) * 100).toFixed(2) + '%'
    : '0%'
)

// ── 生命周期 ──────────────────────────────────────────────

onMounted(() => {
  console.log('[Dashboard] 挂载完成')

  // 1. 初始化图表(需要 DOM)
  initChart()

  // 2. 建立 WebSocket 连接
  connectWebSocket()

  // 3. 初始加载数据
  fetchInitialMetrics()
})

// keep-alive 激活时(如从其他 Tab 切换回来)
onActivated(() => {
  console.log('[Dashboard] 激活,刷新数据')
  // 重新连接 WebSocket(可能因为 Tab 切换断开了)
  if (!isConnected.value) {
    connectWebSocket()
  }
  fetchInitialMetrics()  // 刷新数据
})

// keep-alive 停用时
onDeactivated(() => {
  console.log('[Dashboard] 停用,暂停更新')
  // 停用时断开 WebSocket 节省资源(激活时重连)
  wsConnection.value?.close()
  isConnected.value = false
})

// 卸载前:最后一次 DOM 访问
onBeforeUnmount(() => {
  console.log('[Dashboard] 即将卸载')
  // 保存当前状态到 localStorage(可选)
  localStorage.setItem('dashboard-last-metrics', JSON.stringify(metrics))
})

// 卸载时:清理所有资源
onUnmounted(() => {
  console.log('[Dashboard] 已卸载,清理资源')
  // 1. 销毁图表实例(ECharts 需要手动销毁)
  chartInstance.value?.dispose()

  // 2. 关闭 WebSocket
  wsConnection.value?.close()
})

// 捕获子组件错误
onErrorCaptured((error) => {
  hasError.value = true
  errorMsg.value = error.message
  console.error('[Dashboard] 子组件错误:', error)
  return false  // 阻止继续向上传播
})

// ── 使用 Composable 的定时器(自动清理)────────────────────
// 每 30 秒刷新一次指标(keep-alive 停用时不执行,因为用了 onActivated 管理)
useInterval(() => {
  if (isConnected.value) return  // WebSocket 已连,不需要轮询
  fetchInitialMetrics()
}, 30_000)

// ── 业务逻辑 ──────────────────────────────────────────────

function initChart() {
  const el = document.getElementById('main-chart')
  if (!el) return
  // chartInstance.value = echarts.init(el)
  // 更新图表选项...
  console.log('图表初始化完成')
}

function connectWebSocket() {
  try {
    // wsConnection.value = new WebSocket('wss://api.example.com/realtime')
    // wsConnection.value.onopen = () => { isConnected.value = true }
    // wsConnection.value.onmessage = handleMessage
    // wsConnection.value.onclose = () => { isConnected.value = false }
    isConnected.value = true  // 模拟
    console.log('WebSocket 已连接')
  } catch (e) {
    console.error('WebSocket 连接失败', e)
  }
}

function handleMessage(event: MessageEvent) {
  const data = JSON.parse(event.data)
  Object.assign(metrics, data)
  lastUpdateTime.value = new Date()
  updateChart()
}

function updateChart() {
  if (!chartInstance.value) return
  // chartInstance.value.setOption({ ... })
}

async function fetchInitialMetrics() {
  try {
    // const res = await fetch('/api/metrics')
    // const data = await res.json()
    // Object.assign(metrics, data)
    // 模拟数据
    Object.assign(metrics, {
      visitors: Math.floor(Math.random() * 10000),
      orders:   Math.floor(Math.random() * 500),
      revenue:  Math.floor(Math.random() * 100000),
      errors:   Math.floor(Math.random() * 10),
    })
    lastUpdateTime.value = new Date()
  } catch (e) {
    console.error('获取指标失败', e)
  }
}
</script>

<template>
  <div class="dashboard" :style="{ width: width + 'px' }">
    <div v-if="hasError" class="error-banner">
      ⚠️ 出现错误:{{ errorMsg }}
      <button @click="hasError = false">×</button>
    </div>

    <header>
      <h1>📊 实时数据大屏</h1>
      <span :class="['status', isConnected ? 'online' : 'offline']">
        {{ isConnected ? '● 实时' : '○ 离线' }}
      </span>
      <span v-if="lastUpdateTime" class="update-time">
        更新于 {{ lastUpdateTime.toLocaleTimeString() }}
      </span>
    </header>

    <div class="metrics-grid">
      <div class="metric-card">
        <h3>今日访客</h3>
        <p class="number">{{ metrics.visitors.toLocaleString() }}</p>
      </div>
      <div class="metric-card">
        <h3>今日订单</h3>
        <p class="number">{{ metrics.orders.toLocaleString() }}</p>
      </div>
      <div class="metric-card">
        <h3>今日收入</h3>
        <p class="number">¥{{ metrics.revenue.toLocaleString() }}</p>
      </div>
      <div class="metric-card">
        <h3>转化率</h3>
        <p class="number">{{ conversionRate }}</p>
      </div>
    </div>

    <div id="main-chart" style="height: 400px;" />

    <button @click="fetchInitialMetrics">手动刷新</button>
  </div>
</template>

<style scoped>
.dashboard { padding: 1.5rem; background: #0a0f1e; color: white; min-height: 100vh; }
.status.online  { color: #42b883; }
.status.offline { color: #ff6b6b; }
.metrics-grid   { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
.metric-card    { background: #1a2035; padding: 1rem; border-radius: 8px; }
.number         { font-size: 2rem; font-weight: bold; color: #42b883; }
.error-banner   { background: #ff6b6b; padding: 0.5rem 1rem; border-radius: 4px; }
</style>

总结

钩子 执行时机 主要用途
setup() 实例创建时(同步) 初始化数据、watch、computed
onBeforeMount 挂载前 极少使用
onMounted DOM 就绪后 初始化第三方库、DOM 操作、发请求
onBeforeUpdate 数据变、DOM 未更新 保存更新前 DOM 状态
onUpdated DOM 更新后 读取新 DOM(勿改数据!)
onBeforeUnmount 卸载前 最后一次 DOM 访问
onUnmounted 销毁后 清理定时器、关闭连接、移除监听
onActivated keep-alive 激活 刷新数据、重连
onDeactivated keep-alive 停用 暂停操作、节省资源
onErrorCaptured 子组件报错 错误边界处理

核心原则:在 onMounted 里做初始化,在 onUnmounted 里做清理,把逻辑抽进 Composable 让代码更内聚。

下一篇预告:Vue Router 4 实战——路由配置、动态路由、路由守卫、useRouteuseRouter,以及如何实现权限控制。


💬 你有没有遇到过忘记在 onUnmounted 里清理定时器导致内存泄漏的问题? 欢迎评论区分享!

🙏 如果这篇帮到你,点赞 + 收藏,系列持续更新!

更多推荐