六维教程

Vue3 生命周期

组件从”被创建”到”从页面移除”会经历几个阶段,Vue 在关键节点提供了钩子函数,让我们在正确时机做正确的事,这些钩子都以 on 开头。

组件的一生

一个组件的大致旅程:

初始化 → 挂载(mount) → 更新(update,可多次) → 卸载(unmount)

对应组合式 API 的钩子:

钩子 触发时机 典型用法
onBeforeMount DOM 即将挂载,此时还访问不到元素 挂载前的准备
onMounted DOM 已挂载,可正常访问模板元素与数据 首屏拉数据、初始化第三方库
onBeforeUpdate 数据变化后、DOM 更新前 记录更新前置状态
onUpdated DOM 更新完成后 依赖新 DOM 的逻辑
onBeforeUnmount 组件即将卸载(销毁前) 清理前的通知
onUnmounted 组件已卸载,DOM 已移除 清理定时器、取消监听、断开连接

用得最多的是 onMountedonUnmounted,这两个配合几乎是”组件资源管理”的标准姿势。

为什么没有创建阶段的钩子

细心的同学可能发现:上面表格里没有”创建时”的钩子。

因为不需要<script setup> 的顶层代码本身就运行在组件创建阶段,想”刚创建就做初始化”,直接写在顶层即可。

更新阶段

生命周期表的”更新”阶段(onBeforeUpdate / onUpdated)背后有个坑。

当数据改变的时候,DOM 不会立刻跟着变,Vue 会把多次改动攒到一起、异步刷新。

来看一个例子,想要知道list 列表有多高,box 通过模板引用指到这个列表元素上(模板引用在 Vue3 模板引用 讲过),改完数据立刻读高度,会拿到旧值:

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

const list = ref([1])
const box = useTemplateRef('box')   // 模板引用:指到下面 <div ref="box">

const addItem = () => {
  list.value.push(2)              // ① 改了数据,但 DOM 还没刷新
  console.log(box.value?.offsetHeight)  // ② 立刻读 → 还是旧高度
}
</script>

<template>
  <div ref="box">高度随列表变化:{{ list }}</div>
  <button @click="addItem">加一项</button>
</template>

那改完数据,想立刻读新 DOM怎么办?

nextTick,它是 Vue 提供的”等本次更新刷完再继续“的工具。

await nextTick()

使用nextTick等待刷新完成,再读新 DOM:

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

const list = ref([1])
const box = useTemplateRef('box')   // 模板引用([上一节课](/122.html)讲过:挂载后才有值)

const addItem = async () => {
  list.value.push(2)                   // ① 改数据:此时 DOM 还是旧高度
  await nextTick()                     // ② 等这次刷新完成(时机由 Vue 定)
  console.log(box.value?.offsetHeight)  // ③ 才执行 → 拿到的就是新高度
}
</script>

<template>
  <div ref="box">高度随列表变化:{{ list }}</div>
  <button @click="addItem">加一项</button>
</template>

await nextTick() 究竟做了什么:

  1. nextTick() 返回一个 Promise,这个 Promise 在”刚才攒下的那次刷新完成之后”才 resolve,它做的事就是”确认这次刷新结束了”。
  2. await 让下一行代码挂起,等上面那次刷新完成再继续。

合起来:await nextTick() 的意思是 “等我这次改的数据在 DOM 上反映出来,再往下走”

它保证下面代码读到的 DOM 是新的,但对”什么时候刷”不做任何强推,刷新时机由 Vue 决定

await nextTick() 之后的代码又改数据了呢?

这就是容易绕晕的地方,注意一条关键规则:一次 DOM 更新覆盖的是”一段连续的同步代码”里发生的所有改动,而 await 会把这根同步线拦腰切断

const addItem = async () => {
  list.value.push(2)      // 段 1:同步代码,改动被攒下 → 一次刷新 A
  await nextTick()        // 段 1 到此为止,下面的代码不再属于这段

                          // 段 2(等刷新 A 完成后才执行,是新的同步代码)
  list.value.push(3)      // 又一次改动 → 独立攒下 → 需要一次新的刷新 B
  await nextTick()        // 想读 B 的结果,就得再等一次
}

看出区别了吧,list.push(3) 不是发生在”原来那根同步线”上,它发生在 await 之后的新执行段里。

Vue 只把”同一段同步代码”的改动合并成一次刷新,段 2 的改动攒的是新的一轮 B,和 A 无关。

所以想等 B,必须再次 await nextTick()

一句话:nextTick 只管调用它之前那段同步代码攒下的更新。

完整例子

计时器必须在卸载时清理,否则组件没了定时器还一直跑,会内存泄漏:

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

const seconds = ref(0)
let timer = null

onMounted(() => {
  // 挂载后启动定时器
  timer = setInterval(() => {
    seconds.value++
  }, 1000)
})

onBeforeUnmount(() => {
  // 卸载前清掉,避免泄漏
  clearInterval(timer)
})
</script>

<template>
  <p>已在当前页面停留 {{ seconds }} 秒</p>
</template>

用哪个清理

清理资源(定时器、监听器、WebSocket)用 onBeforeUnmountonUnmounted 均可,两者都在卸载阶段触发,只差”确认 DOM 移除”这一步。

建议用 onBeforeUnmount更常见,因为此时组件实例仍是完整的。

特殊钩子

钩子 时机 何时用到
onActivated/onDeactivated 配合 <KeepAlive> 缓存时,组件被”激活/停用” 缓存页面的恢复/挂起处理(路由进阶详讲)
onErrorCaptured 后代组件抛错被捕获 错误边界、上报
onServerPrefetch SSR 服务端渲染预取数据 只在服务端渲染项目用到

onActivated 这对钩子现在只需知道”存在”,等学到 KeepAlive(动态组件与缓存篇)再回来接上。

注意事项

上面计时器例子已经见过钩子的基本写法,这里补几个写钩子时容易踩的点,都是从实际场景来的:。

可以写多个同阶段钩子

组件挂载后既想拉数据、又想初始化图表,拆成两个 onMounted 完全可以,会按顺序执行,互不干扰:

onMounted(() => { fetchData() })
onMounted(() => { initChart() })

比都堆在一个回调里清晰,这也是”组合式”的初衷。

钩子函数不能随意限制执行时机

钩子函数onXxx(() => {}) 不能放进 if 里、放进意外提前return 的代码后面,或者等 await 之后再写。

因为onXxx 内部会去找”当前正在创建的这个组件”,把你的回调追加到这个组件实例对应的钩子队列里,队列在组件创建时产生,到那刻(挂载/更新/卸载) Vue 会依次执行队列里的回调。

所以执行 onXxx() 这行本身,必须发生在”当前组件正在创建”期间。

放进 if 里等于”可能根本不执行这行”,回调就漏登记了,这样写会被警告:

if (someFlag) {
  onMounted(() => { ... })   // 警告:注册不能放在条件判断里
}

想”满足某个条件才注册”,应该在注册时用条件表达式选回调(哪怕选个空函数),而不是把注册本身藏进 if

钩子跟着时机走

拉数据就放 onMounted,清理就放 onBeforeUnmount,钩子跟”该干什么的时机”天然绑定。

别为了少写几行代码,把该在 onMounted 做的事硬挪到别处,一挪逻辑就悬空了,排查起来难受。

上一篇
Vue3 模板引用
下一篇
Vue3 组件通信入门