一、什么是生命周期?
你在页面上看到的每一个Vue组件,都不是凭空出现的。它有自己的“一生”:
先被创建出来(准备好数据)
然后挂载到页面上(用户能看到)
数据变化时更新(重新渲染)
最后从页面移除时销毁(释放资源)
整个过程就叫“生命周期”。Vue在每个关键节点,都会自动调用一个特定的函数,告诉你“到这一步了,你要不要做点什么”。这些函数就叫生命周期钩子。
用人话讲:生命周期钩子就像闹钟,到点了就响。你只要提前设置好“闹钟响了干什么”,Vue会在对应的时间点自动执行你的代码。
二、Vue3生命周期钩子全家福
Vue3组合式API里有这些钩子,按执行顺序排列:
| 钩子函数 | 执行时机 | 常用程度 |
|---|---|---|
| setup | 组件初始化,最先执行 | ★★★★★ |
| onBeforeMount | 即将挂载到DOM | ★★☆☆☆ |
| onMounted | 已经挂载到DOM | ★★★★★ |
| onBeforeUpdate | 数据变了,但DOM还没更新 | ★★☆☆☆ |
| onUpdated | 数据变了,DOM已更新 | ★★☆☆☆ |
| onBeforeUnmount | 即将从DOM移除 | ★★★★★ |
| onUnmounted | 已经从DOM移除 | ★★☆☆☆ |
| onActivated | 被缓存后重新显示 | ★★★☆☆ |
| onDeactivated | 被缓存后隐藏 | ★★★☆☆ |
下面我们一个一个用案例讲清楚。
三、挂载阶段:组件从无到有
3.1 setup —— 一切的起点
setup是所有逻辑开始的地方。在Vue3的<script setup>语法里,你写的代码默认就是在setup里执行的。
vue
<template> <div> <p>{{ greeting }}</p> </div> </template> <script setup> // 这整个 <script setup> 块,就是在 setup 函数里执行的 // setup 是最先执行的,比所有生命周期钩子都早 import { ref } from 'vue' // 1. 第一步:定义响应式数据 // ref 创建一个响应式变量,值是字符串 '你好,世界' const greeting = ref('你好,世界') // 2. 第二步:定义方法 function sayHi() { console.log('setup 阶段定义的方法') } // 3. 这里可以打印一下,验证 setup 最先执行 console.log('① setup 执行了,组件开始初始化') // 此时组件还没挂载到页面上,所以还看不到 DOM 元素 </script>注意事项:
setup里不能操作 DOM,因为组件还没挂上去适合做:定义数据、定义方法、引入其他组件
不适合做:发网络请求(虽然能做,但更推荐在
onMounted里做)
3.2 onBeforeMount —— 马上要挂载了
这个钩子在组件挂载到页面之前触发。此时模板已经编译好了,但还没插入到 DOM 里。
vue
<template> <div> <p ref="myP">我是一个段落</p> </div> </template> <script setup> // 引入 onBeforeMount 钩子 import { onBeforeMount, ref } from 'vue' // 创建一个 ref,用来获取 DOM 元素 // ref(null) 初始值是 null,后面会绑定到模板里的 p 元素上 const myP = ref(null) // onBeforeMount 接收一个回调函数,在挂载前执行 onBeforeMount(() => { console.log('② onBeforeMount:组件马上就要挂载了') // 尝试获取 DOM 元素 console.log('尝试获取 p 元素:', myP.value) // 输出结果是 null!因为还没挂载,DOM 不存在 // 所以这个阶段不能操作 DOM,什么都拿不到 }) </script>注意事项:
不能操作 DOM,因为组件还没插入页面
这个钩子在服务端渲染(SSR)时也会执行,所以适合做一些不依赖浏览器的初始化
实际项目中这个钩子用得比较少
3.3 onMounted —— 组件已经挂载好了(重点)
这是最常用的钩子之一。组件已经插入到 DOM 里,你现在可以安全地操作 DOM 元素、发网络请求、初始化第三方库了。
vue
<template> <div> <!-- 给 p 元素设置 ref="myP",方便在 JS 里获取它 --> <p ref="myP">初始文本</p> <p>用户名:{{ username }}</p> </div> </template> <script setup> // 引入 onMounted 钩子 import { onMounted, ref } from 'vue' // 定义响应式数据 const username = ref('加载中...') // 创建 ref 用来获取 p 元素 const myP = ref(null) // onMounted 接收一个回调函数,在挂载完成后执行 onMounted(() => { console.log('③ onMounted:组件已经挂载到页面上了') // 1. 现在可以安全操作 DOM 了 console.log('p 元素的内容:', myP.value.textContent) // 输出:'初始文本' // 2. 可以修改 DOM myP.value.style.color = 'red' // 页面上的 p 元素文字变红了 // 3. 适合在这里发网络请求获取数据 // 模拟一个请求:1秒后更新用户名 setTimeout(() => { username.value = '小明' // 页面会自动从 '加载中...' 变成 '小明' }, 1000) // 实际项目里会这样写: // fetch('/api/user').then(res => res.json()).then(data => { // username.value = data.name // }) }) </script>注意事项:
可以安全操作 DOM
最适合发初始数据请求的地方
如果你的子组件也用了
onMounted,父组件的onMounted会在所有子组件挂载完成后才触发
四、更新阶段:数据变了,页面跟着变
4.1 onBeforeUpdate —— 数据变了,但页面还是旧的
当你修改了响应式数据,Vue 会重新渲染 DOM。但在渲染之前,会先触发onBeforeUpdate。
vue
<template> <div> <p ref="countP">计数:{{ count }}</p> <button @click="count++">加1</button> </div> </template> <script setup> // 引入 onBeforeUpdate 钩子 import { onBeforeUpdate, ref } from 'vue' // 定义响应式数据,初始值为 0 const count = ref(0) // 获取 p 元素的引用 const countP = ref(null) // onBeforeUpdate 在数据变化后、DOM 更新前触发 onBeforeUpdate(() => { console.log('④ onBeforeUpdate:数据变了,但 DOM 还是旧的') // 此时 count.value 已经是新值了(比如从 0 变成了 1) console.log('新的 count 值:', count.value) // 输出 1 // 但 DOM 还没更新,页面显示的还是旧值 console.log('页面上显示的:', countP.value.textContent) // 输出:'计数:0' —— 还是旧的! }) </script>注意事项:
可以拿到更新前的 DOM 状态
不要在这里修改数据,可能导致无限循环
实际项目中用得不多,主要用于调试或特殊需求
4.2 onUpdated —— DOM 已经更新完了
数据变化导致 DOM 重新渲染完成后触发。
vue
<template> <div> <p ref="countP">计数:{{ count }}</p> <button @click="count++">加1</button> </div> </template> <script setup> // 引入 onUpdated 钩子 import { onUpdated, ref } from 'vue' const count = ref(0) const countP = ref(null) // onUpdated 在 DOM 更新完成后触发 onUpdated(() => { console.log('⑤ onUpdated:DOM 已经更新完成') // 现在 DOM 里显示的是新值了 console.log('页面上显示的:', countP.value.textContent) // 输出:'计数:1' —— 已经是新的了! }) // 注意:onUpdated 会在每次数据变化导致 DOM 更新后都触发 // 所以不要在里面修改响应式数据,否则会触发新一轮更新,造成死循环 </script>注意事项:
不要在这里修改响应式数据,会导致无限更新循环
如果需要根据 DOM 变化做操作(比如重新初始化图表),可以在这里做
实际项目中用得不如
onMounted多
五、销毁阶段:组件从页面移除
5.1 onBeforeUnmount —— 组件即将被移除(重点)
当组件因为v-if变成false或路由跳转而被移除时,会先触发这个钩子。这是做清理工作的最佳时机。
vue
<template> <div> <p>当前时间:{{ now }}</p> </div> </template> <script setup> // 引入 onBeforeUnmount 和 onMounted import { onBeforeUnmount, onMounted, ref } from 'vue' const now = ref('') let timer = null // 用来存放定时器的 ID // 组件挂载后,开启一个定时器,每秒更新时间 onMounted(() => { console.log('挂载成功,开启定时器') timer = setInterval(() => { // 每秒更新当前时间 now.value = new Date().toLocaleTimeString() }, 1000) }) // 组件即将被销毁前,必须清理定时器 onBeforeUnmount(() => { console.log('⑥ onBeforeUnmount:组件要销毁了,赶紧清理') // 如果不清除定时器,即使组件不在了,定时器还会一直跑 // 会造成内存泄漏,甚至报错 if (timer) { clearInterval(timer) // 清除定时器 timer = null // 把变量也清掉,防止后续误用 } }) </script>注意事项:
必须在这里清理定时器、取消网络请求、移除事件监听
如果不清理,组件销毁后定时器还在跑,会造成内存泄漏
和
onMounted是黄金搭档:一个挂载时创建,一个销毁前清理
5.2 onUnmounted —— 组件已经销毁了
组件从 DOM 移除后触发。此时组件已经完全不存在了。
vue
<script setup> import { onUnmounted } from 'vue' onUnmounted(() => { console.log('⑦ onUnmounted:组件已经彻底销毁') // 大部分清理工作建议在 onBeforeUnmount 里做 // 这个钩子主要是用于确认销毁,或者做一些最终的日志记录 }) </script>六、与 KeepAlive 配合的两个特殊钩子
Vue 有个内置组件叫<KeepAlive>,它可以把组件缓存起来,切换走时不销毁,切换回来时直接用缓存。被缓存的组件会多出两个钩子:
6.1 onActivated —— 组件被激活(显示)
vue
<template> <div>我是被缓存的组件</div> </template> <script setup> import { onActivated } from 'vue' // 被 KeepAlive 包裹的组件,每次从缓存中恢复显示时触发 onActivated(() => { console.log('组件从缓存中恢复了,又能看到我了') // 可以在这里重新获取数据,保证数据是最新的 // 比如:fetchLatestData() }) </script>6.2 onDeactivated —— 组件被失活(隐藏)
vue
<script setup> import { onDeactivated } from 'vue' // 被 KeepAlive 包裹的组件,每次被缓存隐藏时触发 onDeactivated(() => { console.log('组件被缓存了,暂时隐藏') // 可以在这里暂停一些操作,比如暂停播放器、停止轮询 // 比如:pauseVideo() }) </script>父组件配合 KeepAlive 使用:
vue
<template> <div> <button @click="show = !show">切换显示</button> <!-- KeepAlive 包裹的组件不会被销毁,而是被缓存 --> <KeepAlive> <MyComponent v-if="show" /> </KeepAlive> </div> </template> <script setup> import { ref } from 'vue' import MyComponent from './MyComponent.vue' const show = ref(true) </script>七、完整执行顺序
当一个组件从创建到销毁(不用KeepAlive),完整顺序是:
text
① setup —— 初始化数据和方法 ② onBeforeMount —— 即将挂载,DOM不可用 ③ onMounted —— 挂载完成,DOM可用,可以发请求 ... 组件正常运行 ... ④ onBeforeUpdate —— 数据变了,DOM还没更新 ⑤ onUpdated —— DOM更新完成 ... 可能多次触发 ④⑤ ... ⑥ onBeforeUnmount —— 组件即将销毁,清理定时器/事件 ⑦ onUnmounted —— 组件已销毁
记忆口诀:创建先setup,挂载前后跑;更新数据变,销毁要打扫。
八、实战案例:倒计时组件(完整版)
用生命周期做一个功能完整的倒计时组件:
vue
<template> <div class="countdown"> <h3>倒计时组件</h3> <!-- 显示倒计时数字 --> <p class="number">{{ countDown }}</p> <!-- 按钮区域 --> <div class="buttons"> <!-- 如果正在倒计时,显示暂停按钮;否则显示开始按钮 --> <button @click="isRunning ? pause() : start()"> {{ isRunning ? '暂停' : '开始' }} </button> <!-- 重置按钮,回到60秒 --> <button @click="reset">重置</button> </div> </div> </template> <script setup> // 引入需要的钩子和 API import { ref, onMounted, onBeforeUnmount } from 'vue' // -------- 数据定义 -------- const countDown = ref(60) // 倒计时从 60 秒开始 const isRunning = ref(false) // 是否正在运行 let timer = null // 定时器 ID,初始为空 // -------- 方法定义 -------- // 开始倒计时 function start() { // 防止重复点击,已经运行就不再创建新定时器 if (timer) return isRunning.value = true // 标记为运行状态 // setInterval 返回一个定时器 ID,用来后面清除 timer = setInterval(() => { // 每秒执行一次 if (countDown.value > 0) { // 还没到 0,减 1 countDown.value-- } else { // 到 0 了,停止倒计时 clearInterval(timer) // 清除定时器 timer = null // 清空变量 isRunning.value = false // 标记为停止状态 } }, 1000) // 1000 毫秒 = 1 秒 } // 暂停倒计时 function pause() { if (timer) { clearInterval(timer) // 清除定时器 timer = null // 清空变量 } isRunning.value = false // 标记为停止状态 } // 重置倒计时 function reset() { // 先清除现有定时器 if (timer) { clearInterval(timer) timer = null } countDown.value = 60 // 重置为 60 秒 isRunning.value = false // 停止状态 } // -------- 生命周期钩子 -------- // 组件挂载后,自动开始倒计时 onMounted(() => { console.log('倒计时组件挂载成功') start() // 自动开始倒计时 }) // 组件销毁前,必须清除定时器 onBeforeUnmount(() => { console.log('倒计时组件即将销毁,清理定时器') // 如果不清除,组件都没了定时器还在跑,会内存泄漏 if (timer) { clearInterval(timer) timer = null } }) </script> <style scoped> .countdown { text-align: center; padding: 20px; border: 1px solid #ccc; border-radius: 8px; } .number { font-size: 48px; font-weight: bold; margin: 20px 0; } .buttons button { margin: 0 10px; padding: 8px 20px; cursor: pointer; } </style>配合父组件使用,用 v-if 控制销毁:
vue
<template> <div> <button @click="showCountdown = !showCountdown"> {{ showCountdown ? '销毁' : '显示' }}倒计时组件 </button> <!-- v-if 为 false 时,组件被销毁,触发 onBeforeUnmount --> <Countdown v-if="showCountdown" /> </div> </template> <script setup> import { ref } from 'vue' import Countdown from './Countdown.vue' const showCountdown = ref(true) </script>九、练习题
选择题
以下哪个钩子在组件挂载到 DOM 之后触发?
A. setup
B. onBeforeMount
C. onMounted
D. onBeforeUnmount发网络请求获取初始数据,放在哪个钩子最合适?
A. setup
B. onBeforeMount
C. onMounted
D. onUpdated组件销毁前,清除定时器应该放在哪个钩子?
A. onMounted
B. onBeforeUpdate
C. onBeforeUnmount
D. onUnmounted
判断题
每次组件的数据变化导致 DOM 更新,onUpdated 都会触发。( )
onBeforeUnmount 在 onUnmounted 之后触发。( )
setup 阶段可以安全地操作 DOM 元素。( )
简答题
为什么清除定时器要在 onBeforeUnmount 里做?不在 onUnmounted 里做行不行?
编程题
写一个实时时钟组件:
显示当前时间,格式为
HH:MM:SS组件挂载时开始计时,每秒更新
组件销毁时清除定时器
改进题:在题8的基础上,增加一个“暂停/继续”按钮:
点击按钮切换暂停状态
暂停时定时器清除
继续时重新开启定时器
记得在销毁时处理定时器
十、答案
C。onMounted 在 DOM 挂载完成后触发。
C。onMounted 是最佳选择,此时 DOM 已可用,且符合用户预期(先看到页面再加载数据)。虽然 setup 里也能发请求,但那时 DOM 还没出来。
C。onBeforeUnmount 是清理工作的最佳时机,组件还能正常使用,但马上就要销毁了。onUnmounted 也行,但没必要等到彻底销毁才清理。
正确。只要响应式数据变化导致 DOM 重新渲染,onUpdated 就会执行。
错误。onBeforeUnmount 先执行(销毁前),然后才是 onUnmounted(销毁后)。
错误。setup 阶段组件还没挂载,无法获取 DOM 元素,此时 ref 绑定的 DOM 值是 null。
参考答案:在 onBeforeUnmount 里做清理是最佳实践。此时组件还没销毁,可以正常操作,保证清理成功。在 onUnmounted 里做也行,但没必要等到完全销毁再清理。关键是必须清理,不然定时器会一直运行,造成内存泄漏。
参考实现:
vue
<template> <div> <h2>当前时间</h2> <p style="font-size: 36px;">{{ currentTime }}</p> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue' // 定义响应式数据,存储当前时间字符串 const currentTime = ref('') // 用来存定时器 ID let timer = null // 更新时间的方法 function updateTime() { // 获取当前时间对象 const now = new Date() // 获取小时、分钟、秒,不足两位前面补 0 const hours = String(now.getHours()).padStart(2, '0') const minutes = String(now.getMinutes()).padStart(2, '0') const seconds = String(now.getSeconds()).padStart(2, '0') // 拼接成 HH:MM:SS 格式 currentTime.value = `${hours}:${minutes}:${seconds}` } onMounted(() => { // 先立即更新一次,避免一开始显示空白 updateTime() // 然后每秒更新 timer = setInterval(updateTime, 1000) }) onBeforeUnmount(() => { // 销毁时清理定时器 if (timer) { clearInterval(timer) timer = null } }) </script>参考实现:
vue
<template> <div> <h2>实时时钟</h2> <p style="font-size: 36px;">{{ currentTime }}</p> <!-- 按钮文字根据运行状态切换 --> <button @click="toggle"> {{ isRunning ? '暂停' : '继续' }} </button> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue' const currentTime = ref('') const isRunning = ref(false) // 默认暂停状态 let timer = null function updateTime() { const now = new Date() const hours = String(now.getHours()).padStart(2, '0') const minutes = String(now.getMinutes()).padStart(2, '0') const seconds = String(now.getSeconds()).padStart(2, '0') currentTime.value = `${hours}:${minutes}:${seconds}` } // 开启定时器 function startTimer() { // 防止重复创建定时器 if (timer) return updateTime() // 先立即更新一次 timer = setInterval(updateTime, 1000) isRunning.value = true } // 暂停定时器 function stopTimer() { if (timer) { clearInterval(timer) timer = null } isRunning.value = false } // 切换暂停/继续 function toggle() { if (isRunning.value) { stopTimer() } else { startTimer() } } // 挂载后自动开始 onMounted(() => { startTimer() }) // 销毁前清理 onBeforeUnmount(() => { if (timer) { clearInterval(timer) timer = null } }) </script>写在最后
生命周期钩子是 Vue 的骨架,搞懂了它,你就知道每个阶段该干什么:
初始化数据在
setup发请求、操作 DOM 在
onMounted清理定时器、取消请求在
onBeforeUnmount
刚学不用死记硬背,多写几个组件,尤其是带定时器、带请求的,自然就记住了。
有疑问评论区找我,下篇见!