生命周期钩子

文章目录

  • 前言
  • 一、生命周期概览
    • [1.1 Vue 3 完整生命周期](#1.1 Vue 3 完整生命周期)
    • [1.2 Vue 2 → Vue 3 钩子更名](#1.2 Vue 2 → Vue 3 钩子更名)
    • [1.3 Options API vs Composition API](#1.3 Options API vs Composition API)
  • 二、各钩子详解
    • [2.1 setup(替代 beforeCreate / created)](#2.1 setup(替代 beforeCreate / created))
    • [2.2 onBeforeMount](#2.2 onBeforeMount)
    • [2.3 onMounted](#2.3 onMounted)
    • [2.4 onBeforeUpdate / onUpdated](#2.4 onBeforeUpdate / onUpdated)
    • [2.5 onBeforeUnmount / onUnmounted](#2.5 onBeforeUnmount / onUnmounted)
    • [2.6 nextTick 与生命周期的关系](#2.6 nextTick 与生命周期的关系)
    • [2.7 其他钩子(了解即可)](#2.7 其他钩子(了解即可))
  • 三、错误处理钩子
    • [3.1 onErrorCaptured](#3.1 onErrorCaptured)
    • [3.2 应用场景](#3.2 应用场景)
  • [四、keep-alive 特殊生命周期](#四、keep-alive 特殊生命周期)
    • [4.1 onActivated / onDeactivated](#4.1 onActivated / onDeactivated)
    • [4.2 与 onMounted / onUnmounted 的区别](#4.2 与 onMounted / onUnmounted 的区别)
  • 五、父子组件执行顺序
    • [5.1 挂载顺序](#5.1 挂载顺序)
    • [5.2 卸载顺序](#5.2 卸载顺序)
    • [5.3 更新顺序](#5.3 更新顺序)
  • 六、常见应用场景
    • [6.1 数据请求](#6.1 数据请求)
    • [6.2 事件监听与清理](#6.2 事件监听与清理)
    • [6.3 定时器管理](#6.3 定时器管理)
    • [6.4 第三方库初始化](#6.4 第三方库初始化)
    • [6.5 watch 清理与 onUnmounted](#6.5 watch 清理与 onUnmounted)
  • 七、面试聚焦
    • [7.1 为什么父组件 onMounted 在子组件之后?](#7.1 为什么父组件 onMounted 在子组件之后?)
    • [7.2 onUpdated 和 nextTick 有什么区别?](#7.2 onUpdated 和 nextTick 有什么区别?)
    • [7.3 注册多个 onMounted 的执行顺序?](#7.3 注册多个 onMounted 的执行顺序?)
    • [7.4 async setup 会影响生命周期吗?](#7.4 async setup 会影响生命周期吗?)
  • 八、易混淆点
  • 九、思考与练习
  • 总结

前言

生命周期钩子是 Vue 组件从创建到销毁各个阶段自动调用的函数,理解它们的执行时机和应用场景是写出健壮代码的基础。本篇会讲清楚:

  • 各生命周期钩子的执行时机
  • Composition API 中的生命周期
  • 父子组件生命周期执行顺序(挂载 / 更新 / 卸载)
  • keep-alive 缓存组件的特殊生命周期
  • nextTick 与生命周期的配合

一、生命周期概览

1.1 Vue 3 完整生命周期

复制代码
setup()
  ↓
onBeforeMount()    // DOM 挂载前
  ↓
onMounted()        // DOM 挂载后
  ↓
  ┌─────────────────────────────────────┐
  │  响应式数据变化时重复进入(非一次性)   │
  │  onBeforeUpdate() → onUpdated()     │
  └─────────────────────────────────────┘
  ↓
onBeforeUnmount()  // 组件卸载前
  ↓
onUnmounted()      // 组件卸载后

挂载与卸载各执行一次;更新钩子会在每次响应式依赖触发的重渲染中重复触发

1.2 Vue 2 → Vue 3 钩子更名

Vue 2 Vue 3
beforeDestroy beforeUnmount
destroyed unmounted

其余钩子名称基本不变。Vue 3 新增 onActivated / onDeactivated(配合 keep-alive)、onErrorCaptured 等。

1.3 Options API vs Composition API

javascript 复制代码
// Options API(Vue 2 风格,Vue 3 仍可用)
export default {
  beforeCreate() { },  // Composition API 中由 setup 替代,Options API 仍可用
  created() { },       // 同上;setup 中的同步代码等价于 created
  beforeMount() { },
  mounted() { },
  beforeUpdate() { },
  updated() { },
  beforeUnmount() { },
  unmounted() { }
}

// Composition API(Vue 3 推荐)
import { 
  onBeforeMount, onMounted,
  onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted
} from 'vue'

// setup 本身就是 created/beforeCreate 阶段
setup() {
  onBeforeMount(() => { })
  onMounted(() => { })
  onBeforeUpdate(() => { })
  onUpdated(() => { })
  onBeforeUnmount(() => { })
  onUnmounted(() => { })
}

二、各钩子详解

2.1 setup(替代 beforeCreate / created)

vue 复制代码
<script setup>
// setup 就是 created/beforeCreate 阶段
// 此时:props 已解析,DOM 未创建
import { ref } from 'vue'

const count = ref(0)  // ✅ 可以访问 props
// document.getElementById('app')  // ❌ DOM 未创建
</script>

2.2 onBeforeMount

javascript 复制代码
import { onBeforeMount } from 'vue'

onBeforeMount(() => {
  // DOM 挂载前调用
  // 此时:虚拟 DOM 已创建,真实 DOM 未挂载
  // 不能进行 DOM 操作
  console.log('beforeMount')
})

2.3 onMounted

javascript 复制代码
import { ref, onMounted } from 'vue'

const el = ref(null)

onMounted(() => {
  // DOM 挂载后调用
  // 此时:真实 DOM 已挂载,可以进行 DOM 操作
  console.log('mounted')
  
  // 获取 DOM 元素
  console.log(el.value)  // DOM 元素
  
  // 初始化第三方库
  const chart = echarts.init(el.value)
  
  // 异步数据请求
  fetchData()
})

2.4 onBeforeUpdate / onUpdated

javascript 复制代码
import { ref, onBeforeUpdate, onUpdated } from 'vue'

const count = ref(0)

onBeforeUpdate(() => {
  // DOM 更新前调用
  // 此时:响应式数据已变化,DOM 尚未更新
  console.log('beforeUpdate', count.value)
})

onUpdated(() => {
  // DOM 更新后调用
  // 此时:DOM 已更新为最新状态
  console.log('updated', count.value)
  
  // 若需基于最新 DOM 做测量,通常直接读即可
  // 若在 onUpdated 内再次修改响应式数据,需 nextTick 等待下一轮 DOM 更新
})

2.5 onBeforeUnmount / onUnmounted

javascript 复制代码
import { onBeforeUnmount, onUnmounted } from 'vue'

onBeforeUnmount(() => {
  // 组件卸载前调用
  // 此时:组件实例仍然可用
  console.log('beforeUnmount')
})

onUnmounted(() => {
  // 组件卸载后调用
  // 此时:组件实例已销毁
  console.log('unmounted')
  
  // 清理定时器
  clearInterval(timer)
  
  // 取消事件监听
  window.removeEventListener('resize', handler)
  
  // 断开连接
  websocket.close()
})

2.6 nextTick 与生命周期的关系

onMounted / onUpdated 触发时,Vue 已完成当前组件 的 DOM patch,但在以下场景仍可能需要 nextTick

javascript 复制代码
import { ref, onMounted, onUpdated, nextTick } from 'vue'

const list = ref([])
const count = ref(0)

onMounted(async () => {
  list.value = await fetchList()
  // 列表渲染完成后才能正确测量最后一项高度
  await nextTick()
  measureLastItem()
})

onUpdated(async () => {
  // 在 updated 里改了数据,DOM 尚未反映这次变更
  count.value++
  await nextTick()
  // 此时才能读到 count 变更后的 DOM
})
场景 推荐做法
组件首次挂载后操作 DOM onMounted
数据变更后读取/操作更新后的 DOM nextTick(或在 onUpdated 中直接读)
onUpdated 内再次改数据后读 DOM 改完后 await nextTick()

2.7 其他钩子(了解即可)

javascript 复制代码
import { onServerPrefetch, onRenderTracked, onRenderTriggered } from 'vue'

// SSR:在服务端渲染前预取数据
onServerPrefetch(async () => {
  await store.fetchUser()
})

// 开发调试:追踪响应式依赖何时触发渲染(仅 dev)
onRenderTracked((e) => console.log('依赖被追踪', e))
onRenderTriggered((e) => console.log('依赖触发更新', e))

日常业务开发以前面几节的钩子为主,以上三个按需查阅即可。


三、错误处理钩子

3.1 onErrorCaptured

vue 复制代码
<script setup>
import { onErrorCaptured } from 'vue'

// 捕获子组件的错误
onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err)
  console.error('错误组件:', instance)
  console.error('错误信息:', info)
  
  // 返回 false 阻止错误继续向上传播
  return false
})
</script>

3.2 应用场景

javascript 复制代码
// 错误边界组件
import { ref, onErrorCaptured } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  // 阻止错误传播
  return false
})

// 模板中展示降级 UI
// <div v-if="error">出错了: {{ error.message }}</div>
// <slot v-else />

四、keep-alive 特殊生命周期

4.1 onActivated / onDeactivated

vue 复制代码
<script setup>
import { onActivated, onDeactivated } from 'vue'

// keep-alive 缓存的组件激活时调用
onActivated(() => {
  console.log('activated')
  // 重新获取数据或恢复状态
})

// keep-alive 缓存的组件停用时调用
onDeactivated(() => {
  console.log('deactivated')
  // 清理资源
})
</script>

4.2 与 onMounted / onUnmounted 的区别

javascript 复制代码
// 普通组件(无 keep-alive)
// 挂载 → onMounted
// 卸载 → onUnmounted

// keep-alive 缓存的组件
// 首次挂载 → onMounted + onActivated(两者都会触发)
// 切换离开 → onDeactivated(不触发 onUnmounted)
// 切换回来 → onActivated(不触发 onMounted)
// 彻底销毁 → onUnmounted
vue 复制代码
<!-- 父组件 -->
<template>
  <KeepAlive>
    <ChildA v-if="showA" />
    <ChildB v-else />
  </KeepAlive>
</template>

<!-- ChildA 的生命周期: -->
<!-- 首次渲染:onMounted + onActivated -->
<!-- 切换到 B:onDeactivated(不触发 onUnmounted) -->
<!-- 切换回来:onActivated(不触发 onMounted) -->

五、父子组件执行顺序

5.1 挂载顺序

复制代码
父组件 setup
父组件 onBeforeMount
  ↓
  子组件 setup
  子组件 onBeforeMount
  子组件 onMounted
  ↓
父组件 onMounted
vue 复制代码
<!-- 父组件 -->
<script setup>
import { onBeforeMount, onMounted } from 'vue'
import Child from './Child.vue'

onBeforeMount(() => console.log('父 beforeMount'))
onMounted(() => console.log('父 mounted'))
</script>

<!-- 子组件 -->
<script setup>
import { onBeforeMount, onMounted } from 'vue'

onBeforeMount(() => console.log('子 beforeMount'))
onMounted(() => console.log('子 mounted'))
</script>

<!-- 输出顺序: -->
<!-- 父 beforeMount -->
<!-- 子 beforeMount -->
<!-- 子 mounted -->
<!-- 父 mounted -->

5.2 卸载顺序

复制代码
父组件 onBeforeUnmount
  ↓
  子组件 onBeforeUnmount
  子组件 onUnmounted
  ↓
父组件 onUnmounted
javascript 复制代码
// 子组件先卸载,父组件后卸载
// 子组件的清理逻辑在父组件之前执行

5.3 更新顺序

数据变化触发重渲染时,顺序与挂载类似:子组件先完成更新,父组件后完成

复制代码
父组件 onBeforeUpdate
  ↓
  子组件 onBeforeUpdate
  子组件 onUpdated
  ↓
父组件 onUpdated
javascript 复制代码
// 挂载 / 更新:子组件「完成」在前,父组件「完成」在后
// 卸载:子组件「完成」在前,父组件「完成」在后
// 规律:before* 钩子父先子后,*ed / mounted / unmounted 钩子子先父后

六、常见应用场景

6.1 数据请求

数据请求不必须 写在 onMounted,按是否依赖 DOM、是否随路由变化来选:

javascript 复制代码
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const data = ref(null)
const loading = ref(true)

// 方式一:setup 中直接请求(不依赖 DOM,尽早发起)
async function fetchData() {
  loading.value = true
  try {
    const res = await fetch('/api/data')
    data.value = await res.json()
  } finally {
    loading.value = false
  }
}
fetchData()

// 方式二:onMounted(需要 DOM 就绪,或明确「挂载后再请求」)
onMounted(() => {
  fetchData()
})

// 方式三:watch 路由参数(列表 → 详情等场景)
watch(
  () => route.params.id,
  (id) => {
    if (id) fetchDetail(id)
  },
  { immediate: true }
)
写法 适用场景
setup 顶层 / 普通函数 纯数据请求,不依赖 DOM
onMounted 依赖 DOM 尺寸/第三方库,或需等组件挂载后再请求
watch + immediate 路由参数、props 变化时重新请求

6.2 事件监听与清理

javascript 复制代码
import { onMounted, onUnmounted } from 'vue'

const handleResize = () => {
  console.log('窗口大小变化')
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

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

6.3 定时器管理

javascript 复制代码
import { ref, onMounted, onUnmounted } from 'vue'

const count = ref(0)
let timer = null

onMounted(() => {
  timer = setInterval(() => {
    count.value++
  }, 1000)
})

onUnmounted(() => {
  clearInterval(timer)  // 防止内存泄漏
})

6.4 第三方库初始化

javascript 复制代码
import { ref, onMounted, onUnmounted } from 'vue'

const chartRef = ref(null)
let chart = null

const handleResize = () => chart?.resize()

onMounted(() => {
  // 初始化 ECharts
  chart = echarts.init(chartRef.value)
  chart.setOption({ ... })
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
  chart?.dispose()
})

6.5 watch 清理与 onUnmounted

Composition API 中,副作用清理不一定只靠 onUnmounted

javascript 复制代码
import { ref, watch, watchEffect, onUnmounted } from 'vue'

const count = ref(0)

// watch 返回 stop 函数,需手动停止时可调用
const stopWatch = watch(count, (val) => {
  console.log(val)
})

// 组件内 watchEffect 会在组件卸载时自动停止
watchEffect(() => {
  console.log(count.value)
})

onUnmounted(() => {
  stopWatch()  // 提前停止 watch,避免卸载前仍触发
})

组件内的 watch / watchEffect 默认会随组件销毁而停止;跨组件或手动创建的 watcher 才需要显式 stop()


七、面试聚焦

7.1 为什么父组件 onMounted 在子组件之后?

父组件模板渲染时会创建子组件实例。挂载阶段遵循:

  • before* 钩子:父 → 子(自上而下通知即将发生)
  • mounted / updated / unmounted子 → 父(自下而上确认已完成)

因此父组件 onMounted 执行时,子组件 DOM 已挂载完毕,可以在父组件里安全访问子组件 ref。

7.2 onUpdated 和 nextTick 有什么区别?

  • onUpdated :每次响应式触发的 DOM 更新完成后自动调用,可能频繁触发
  • nextTick :在当前 DOM 更新队列 flush 后执行一次回调,按需调用
javascript 复制代码
count.value++           // 触发更新
await nextTick()        // 等待本次更新对应的 DOM patch 完成
// 适合在函数/事件处理器中读取更新后的 DOM

onUpdated(() => { ... }) // 每次数据变化导致 DOM 更新后都会执行

不要在 onUpdated 里无条件修改响应式数据,容易形成更新死循环;若必须修改,加条件判断或改用 nextTick

7.3 注册多个 onMounted 的执行顺序?

javascript 复制代码
onMounted(() => console.log(1))
onMounted(() => console.log(2))
onMounted(() => console.log(3))
// 输出:1 → 2 → 3(按注册顺序依次执行)

同一钩子多次注册会全部执行,顺序与注册顺序一致。

7.4 async setup 会影响生命周期吗?

javascript 复制代码
// setup 可以是 async,但组件会进入 Suspense 边界
export default {
  async setup() {
    const data = await fetchData()
    onMounted(() => {
      // 仍然有效,在挂载后执行
    })
    return { data }
  }
}

async setup 不会阻止 onMounted 等钩子注册;组件在 setup Promise resolve 前不会渲染,需配合 <Suspense> 处理加载态。


八、易混淆点

  1. setup 中的同步代码等价于 created :Composition API 无需再写 created;Options API 中 beforeCreate / created 仍可用。
  2. onMounted ≠ DOM 一定「全部就绪」 :当前组件 DOM 已挂载,但若刚改了列表数据,子节点可能尚未渲染,需 await nextTick() 后再测量。
  3. onUpdated 会多次触发 :不要在回调里无条件改响应式数据;读 DOM 优先用 nextTick,或确保有终止条件。
  4. keep-alive 首次挂载onMountedonActivated 同时触发;缓存切换不触发 onUnmounted
  5. beforeed 的顺序规律beforeMount / beforeUpdate / beforeUnmount 父先子后;mounted / updated / unmounted 子先父后。
  6. 清理资源 :定时器、DOM 事件、watchstop()、第三方实例 dispose() 等,在 onUnmounted(或 onDeactivated)中处理。

九、思考与练习

1. 父组件 onMounted 里能否安全访问子组件 ref?为什么?

解析:可以。子组件 onMounted 先于父组件执行,父组件 onMounted 触发时子组件 DOM 已挂载。

2. 数据请求放在 setuponMounted 各有什么适用场景?

解析:

  • setup:不依赖 DOM,希望尽早发起请求
  • onMounted:依赖 DOM 或第三方库初始化后再请求

3. keep-alive 缓存的组件会触发哪些生命周期?

解析:

  • 首次挂载:onMountedonActivated
  • 切换离开:onDeactivated
  • 切换回来:onActivated
  • 彻底销毁:onUnmounted

4. 响应式数据更新时,父子组件 onBeforeUpdate / onUpdated 的执行顺序?

解析:

  • onBeforeUpdate:父 → 子
  • onUpdated:子 → 父

5. 在事件处理函数里修改数据后立刻读取 DOM,应该用什么?

解析:使用 await nextTick() 等待本次更新 flush 后再读 DOM;onUpdated 适合监听「每次更新完成后」的副作用,不适合在单次事件里替代 nextTick

6. 如何避免内存泄漏?

解析:在 onUnmounted(keep-alive 场景还应在 onDeactivated)中清理资源:

javascript 复制代码
onUnmounted(() => {
  clearInterval(timer)
  window.removeEventListener('resize', handler)
  stopWatch()
  chart?.dispose()
})

总结

  • setup:替代 beforeCreate / created(Composition API),props 已解析,DOM 未创建
  • onMounted :当前组件 DOM 已挂载,适合第三方库初始化;列表类 DOM 操作可能还需 nextTick
  • onUpdated :每次 DOM 更新后触发,避免无条件改状态;与 nextTick 配合读取最新 DOM
  • onUnmounted:清理定时器、事件监听、watch、第三方实例
  • keep-alive :首次 onMounted + onActivated;缓存切换走 onActivated / onDeactivated
  • 父子顺序before* 父先子后;mounted / updated / unmounted 子先父后
  • 数据请求 :不依赖 DOM 可放 setup;依赖 DOM 或挂载后再请求用 onMounted;随路由/props 变化用 watch