组件生命周期:从 @PostConstruct 到 onMounted

为什么要有生命周期钩子

一个组件从"被创建"到"从页面移除",中间会经过好几个阶段。有些事必须在特定时机做

Java 对照

@Component
public class MyBean {
    @PostConstruct      // 构造之后、可用之前
    public void init() { /* 初始化 */ }

    @PreDestroy         // 销毁之前
    public void cleanup() { /* 清理 */ }
}

Vue 的生命周期钩子就是这套"在特定时机执行代码"的机制,只不过切分得更细。


1. 生命周期全景图

                  ┌────────────────────┐
   setup()  ────> │  beforeCreate      │ (选项式 API 才有,setup 里用不到)
                  │  created           │
                  └──────────┬─────────┘
                             │
                             ▼
                  ┌────────────────────┐
                  │  onBeforeMount     │ 挂载前(DOM 还没生成)
                  │  onMounted  ★      │ 挂载后(DOM 可用)
                  └──────────┬─────────┘
                             │
                    (数据变化时循环触发)
                             │
                             ▼
                  ┌────────────────────┐
                  │  onBeforeUpdate    │ 数据变了,DOM 还没改
                  │  onUpdated         │ DOM 已经改完
                  └──────────┬─────────┘
                             │
                             ▼
                  ┌────────────────────┐
                  │  onBeforeUnmount   │ 卸载前(还能访问 DOM)
                  │  onUnmounted ★     │ 卸载后(DOM 没了)
                  └────────────────────┘

打星号的两个(onMounted / onUnmounted)是日常 90% 的场景。其他的钩子偶尔用。


2. setup 本身就是"创建时"

<script setup> 里,顶层代码就相当于 created 钩子——组件创建时执行一次。

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

// 这些代码在组件"被创建"时执行
const count = ref(0)
console.log('setup 执行了一次')
</script>

Java 对照<script setup> 顶层 = 构造函数 + @PostConstruct 的合体

这一点很关键:不需要单独写 created 钩子,直接把代码写在 setup 里就行。这也是 Composition API 比 Options API 清爽的主要原因——少了一半仪式感。


3. onMounted:DOM 已经渲染

最常用的钩子。适用场景:

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

const users = ref([])
const inputRef = ref<HTMLInputElement | null>(null)

onMounted(async () => {
  // 1) 发请求拉数据
  users.value = await fetch('/api/users').then(r => r.json())

  // 2) 操作 DOM
  inputRef.value?.focus()

  console.log('组件已挂载,DOM 可用')
})
</script>

<template>
  <input ref="inputRef" />
  <ul>
    <li v-for="u in users" :key="u.id">{{ u.name }}</li>
  </ul>
</template>

Java 对照

@PostConstruct
public void init() {
    // 发请求、加载配置、绑定监听器
    this.users = userService.fetchAll();
}

为什么不在 setup 里直接发请求?

其实也可以,只要不依赖 DOM:

<script setup>
// 顶层就能发请求,setup 阶段已经能用响应式
const users = ref([])
fetch('/api/users').then(r => r.json()).then(list => users.value = list)
</script>

但把"数据加载"放 onMounted 里的好处:

项目里的约定


4. onUnmounted:组件卸载后

第二常用的钩子。主要用来清理副作用

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

function handleScroll() {
  console.log('scroll', window.scrollY)
}

onMounted(() => {
  window.addEventListener('scroll', handleScroll)
})

onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll)
  // 不清理的话:组件消失了但监听器还在,内存泄漏 + bug
})
</script>

Java 对照

@PreDestroy
public void cleanup() {
    eventBus.unregister(this);
    scheduler.shutdown();
}

典型的"必须清理"清单

Vue 提供的惯用写法watchwatchEffect 返回一个"取消函数"。在 setup 里调用它们会自动在组件卸载时取消,不用手写 onUnmounted。这一点比 Java 贴心——框架帮你兜底。


5. onUpdated:DOM 更新后

比较少用。数据一变组件就重渲染,重渲染完 DOM 更新完会触发 onUpdated

<script setup>
import { onUpdated } from 'vue'

onUpdated(() => {
  console.log('DOM 更新完毕')
})
</script>

典型场景

注意事项


6. onBeforeMount / onBeforeUpdate / onBeforeUnmount

Before 前缀的三个钩子,在对应动作之前触发。日常用得少,但有两个典型场景:

场景 1:onBeforeUnmount 做收尾

有时候"卸载前"还要访问 DOM,这时 DOM 还在:

<script setup>
import { onBeforeUnmount } from 'vue'
onBeforeUnmount(() => {
  // 比如把当前滚动位置存进 sessionStorage,下次进来恢复
  sessionStorage.setItem('scroll', String(window.scrollY))
})
</script>

onUnmounted 阶段 DOM 已经被移除,访问不了。

场景 2:onBeforeUpdate 获取更新前的状态

<script setup>
import { onBeforeUpdate } from 'vue'
onBeforeUpdate(() => {
  console.log('要更新了,当前 DOM 还是旧的')
})
</script>

几乎只在调试时用。


7. 其他生命周期钩子(了解即可)

钩子 触发时机 典型场景
onErrorCaptured 子组件抛错时 错误边界(类比 try-catch)
onActivated <KeepAlive> 缓存的组件被激活 页面切换回来时刷新数据
onDeactivated <KeepAlive> 缓存的组件被停用 暂停定时器
onRenderTracked 渲染时追踪某个依赖(仅开发环境) 调试响应式
onRenderTriggered 某个依赖触发了重渲染(仅开发环境) 调试响应式

onActivated / onDeactivated 只在外层包了 <KeepAlive> 时才有意义。Vue 的 <KeepAlive> 类似 Android 的 Fragment 保活——组件被切走但实例留在内存里。


8. 生命周期 + 父子组件的触发顺序

一个父子组件树,挂载顺序是:

父 setup
  └─ 子 setup
       └─ 子 onBeforeMount
            └─ 子 onMounted
  └─ 父 onBeforeMount
       └─ 父 onMounted

规律:子组件比父组件挂载完。理由:父组件渲染到 <ChildComp /> 时要先把子组件渲染出来,子的 DOM 是父的一部分。

卸载顺序相反:父先 beforeUnmount,然后卸载每个子组件,最后父 unmounted

理解这个顺序,在"父组件传 props、子组件 onMounted 里要用这个 prop" 的场景就不会踩坑——子挂载时父的 props 已经传过来了。


9. 项目里的实例

src/pages/HomePage.vue 目前没用到任何生命周期钩子——所有数据都是静态 import 的,不需要异步加载。

改造示例:假设笔记要从远程 API 拉,应该长这样:

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

const allNotes = ref<NoteListItem[]>([])
const loading = ref(true)

onMounted(async () => {
  try {
    allNotes.value = await fetch('/api/notes').then(r => r.json())
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else>
    <!-- 原有模板 -->
  </div>
</template>

这个模式以后接 AI 聊天接口、加载用户设置时都会反复用到。


10. 新手常见误区

误区 1:在 setup 里访问 DOM

<script setup>
import { ref } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)

// ❌ setup 阶段 DOM 还没生成
inputRef.value?.focus()
</script>

<template>
  <input ref="inputRef" />
</template>

setup 执行完、DOM 才生成,所以 inputRef.value 此时是 null访问 DOM 必须在 onMounted 及之后的钩子里

误区 2:忘了清理

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  setInterval(() => console.log('tick'), 1000)
  // ❌ 页面切走后这个定时器还在跑
})
</script>

修复

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

let timer: number
onMounted(() => {
  timer = window.setInterval(() => console.log('tick'), 1000)
})
onUnmounted(() => clearInterval(timer))
</script>

或者用 VueUse 的 useIntervalFn,自动帮你管理生命周期:

import { useIntervalFn } from '@vueuse/core'
useIntervalFn(() => console.log('tick'), 1000)

误区 3:把所有初始化塞进 onMounted

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

const count = ref(0)
onMounted(() => {
  // 这些根本不需要等 DOM
  console.log('hi')
  fetchConfig()
})
</script>

不需要 DOM 的初始化代码,直接写在 setup 顶层更直观。onMounted 留给真正需要"DOM 已就绪"的操作。


小练习:在 in4vue 里试一遍

  1. HomePage.vueonMounted(() => console.log('首页挂载'))onUnmounted(() => console.log('首页卸载'))
  2. 访问首页、点击卡片进详情页、再回首页,看控制台输出顺序
  3. 在详情页加 onMounted(() => window.scrollTo(0, 0)),体验"新页面每次都回到顶部"的效果
  4. 尝试加 setInterval 然后不清理,反复来回切换首页/详情页,打开 DevTools 的 Performance 录制一段,观察内存和定时器数量是否在涨(验证"必须清理"的真实代价)

延伸阅读