Vue 3 生命周期钩子完全指南
本文档从零开始讲解 Vue 3 的生命周期钩子:组件从创建到销毁会经历哪些阶段、每个阶段对应哪些钩子、在钩子里能做什么和不能做什么、选项式 API 与组合式 API 的写法对比,以及常见用法示例,适合新手系统学习。
一、什么是“生命周期”?
1.1 生活中的类比
一个组件就像一个人:出生(创建)→ 出现在世界上(挂载到页面)→ 成长变化(数据更新、视图更新)→ 离开(从页面移除、销毁)。
Vue 在这些关键时间点提供了“钩子”(函数),你可以在里面写自己的代码,例如:组件挂载好后去请求接口、即将销毁前清除定时器。
1.2 为什么需要钩子?
- created / setup 执行完:数据、计算属性已经有了,但还没有 DOM,不能操作 document.querySelector。
- mounted:组件已经挂载到 DOM,可以安全地操作自己的 DOM、发请求、加事件监听。
- unmounted:组件即将被移除,应在这里移除监听、清定时器,避免内存泄漏。
所以“生命周期”就是组件的几个阶段,“钩子”就是你在每个阶段可以插入的代码。
二、生命周期有哪些阶段?(总览)
Vue 3 组件的生命周期大致分为四段:
- 创建(Creation):组件实例被创建,数据、计算属性等初始化。
- 挂载(Mounting):把组件渲染出来的 DOM 插入到页面上。
- 更新(Updating):响应式数据变化,组件重新渲染。
- 卸载(Unmounting):组件从页面上移除并销毁。
每一段都有“之前”和“之后”的钩子(部分阶段只有“之后”)。下面按顺序说明每个钩子在什么时候执行、适合做什么。
三、选项式 API 的生命周期钩子
在 export default { } 里可以写这些选项(函数):
| 钩子名 | 执行时机 |
|---|---|
| beforeCreate | 组件实例刚创建,data、methods 还未初始化 |
| created | data、methods 等已初始化,但 DOM 还未生成 |
| beforeMount | 即将把组件渲染成 DOM 并插入页面之前 |
| mounted | DOM 已插入页面,可以操作自己的 DOM、发请求 |
| beforeUpdate | 数据变化后,虚拟 DOM 即将重新渲染、打补丁之前 |
| updated | DOM 已更新完成 |
| beforeUnmount | 组件即将被销毁、从页面移除之前 |
| unmounted | 组件已销毁、已从页面移除,最后做清理 |
3.1 beforeCreate
- 时机:组件实例刚被创建,data、methods、computed 都还没有。
- 用途:极少在这里写逻辑,因为拿不到数据。
- 示例:一般不用。
3.2 created
- 时机:data、methods、computed 已经初始化好,可以访问 this.xxx;但还没有 DOM(没有挂载到页面上)。
- 用途:发请求、初始化一些不依赖 DOM 的数据。
- 注意:不能在这里用 document.querySelector 或 this.$refs 拿 DOM,DOM 还不存在。
export default {
data() {
return { list: [] }
},
created() {
console.log('data 已有,DOM 还没有')
this.fetchList()
},
methods: {
async fetchList() {
const res = await fetch('/api/list')
this.list = await res.json()
}
}
}
3.3 beforeMount
- 时机:即将把组件的模板编译成 DOM 并插入到页面之前。
- 用途:很少用;需要“最后一刻改数据再渲染”时才会用。
- 示例:一般不用。
3.4 mounted(最常用之一)
- 时机:组件的 DOM 已经插入到页面,可以认为“组件已经显示在屏幕上了”。
- 用途:
- 操作自己的 DOM(如 focus、测量宽高)。
- 发请求(若 created 没发)。
- 添加需要手动移除的监听(如 window、document 事件),并在 unmounted 里移除。
- 注意:子组件的 mounted 会先于父组件的 mounted 执行(子先挂载完,父再挂载完)。
export default {
data() {
return { list: [] }
},
mounted() {
console.log('DOM 已挂载,可以操作 this.$refs.xxx')
this.$refs.input?.focus()
window.addEventListener('resize', this.onResize)
},
beforeUnmount() {
window.removeEventListener('resize', this.onResize)
},
methods: {
onResize() {
console.log('窗口大小变了')
}
}
}
3.5 beforeUpdate
- 时机:响应式数据变了,Vue 即将根据新数据重新渲染虚拟 DOM 并更新真实 DOM 之前。
- 用途:在 DOM 更新前读一次当前 DOM 状态(如滚动位置),较少用。
- 注意:不要在这里改数据,容易造成无限循环。
3.6 updated
- 时机:数据变化导致的 DOM 更新已经完成。
- 用途:需要“DOM 更新后”再做的操作(如根据新内容滚动到底部);复杂情况更推荐用 nextTick。
- 注意:不要在这里改依赖的响应式数据,容易造成无限循环。
export default {
data() {
return { list: [] }
},
updated() {
console.log('DOM 已更新')
this.$nextTick(() => {
this.$refs.list?.scrollTo(0, this.$refs.list.scrollHeight)
})
}
}
3.7 beforeUnmount
- 时机:组件即将被销毁、从页面移除之前,此时 DOM 还在。
- 用途:做清理前的最后处理(如弹窗确认);清理逻辑一般放在 unmounted 也可以。
3.8 unmounted(最常用之一,做清理)
- 时机:组件已经销毁,已从 DOM 移除。
- 用途:移除事件监听、清除定时器、取消未完成的请求等,避免内存泄漏和报错。
- 注意:此时不能再操作组件 DOM(已经没了);只做“解绑、清理”。
export default {
data() {
return { timer: null }
},
mounted() {
this.timer = setInterval(() => {
console.log('tick')
}, 1000)
},
unmounted() {
if (this.timer) clearInterval(this.timer)
}
}
四、组合式 API 的生命周期钩子(onXxx)
在 <script setup> 或 setup() 里,用 onXxx 注册钩子;没有 beforeCreate / created,因为 setup 本身就在“创建”阶段执行。
| 选项式 API | 组合式 API |
|---|---|
| beforeCreate | 无(setup 即此时) |
| created | 无(setup 即此时) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
4.1 写法示例
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue'
onMounted(() => {
console.log('挂载完成')
})
onUpdated(() => {
console.log('更新完成')
})
onUnmounted(() => {
console.log('即将卸载,做清理')
})
</script>
- 可以写多个同类型钩子,都会按注册顺序执行。
- 一般把“和某块逻辑相关”的清理写在对应的 onUnmounted 里(如上面“mounted 里加监听,unmounted 里移除”)。
4.2 “created”在组合式里怎么写?
setup(或 <script setup> 的顶层)本身就是“创建阶段”:
这里可以 ref、reactive、computed、watch,等价于在选项式里 created 之前数据已经可用。
所以“原来在 created 里发请求”,在组合式里可以:
- 在 setup 顶层直接调一个 async 函数(不 await,或 await 后赋值给 ref);
- 或写在 onMounted 里(若需要 DOM 再发,就放 onMounted)。
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([])
onMounted(async () => {
const res = await fetch('/api/list')
list.value = await res.json()
})
</script>
五、执行顺序:父与子
有父子组件时,顺序大致是:
- 创建:父 beforeCreate → 父 created → 子 beforeCreate → 子 created → …
- 挂载:子 beforeMount → 子 mounted → 父 beforeMount → 父 mounted
- 更新:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated
- 卸载:父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted
即:子先挂载完,父再挂载完;子先卸载完,父再卸载完。
这样在父的 mounted 里可以认为子的 DOM 也已经在了(若子不是异步组件)。
六、常见使用场景示例
6.1 挂载后请求数据(onMounted)
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([])
const loading = ref(true)
onMounted(async () => {
try {
const res = await fetch('/api/list')
list.value = await res.json()
} finally {
loading.value = false
}
})
</script>
6.2 挂载后操作 DOM(onMounted + ref)
<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
6.3 挂载后加监听、卸载时移除(避免泄漏)
<script setup>
import { onMounted, onUnmounted } from 'vue'
function onResize() {
console.log('resize')
}
onMounted(() => {
window.addEventListener('resize', onResize)
})
onUnmounted(() => {
window.removeEventListener('resize', onResize)
})
</script>
6.4 挂载后开定时器、卸载时清除
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const count = ref(0)
let timer = null
onMounted(() => {
timer = setInterval(() => {
count.value++
}, 1000)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
6.5 更新完成后滚动到底部(onUpdated + nextTick)
<script setup>
import { ref, onUpdated, nextTick } from 'vue'
const list = ref([])
const listRef = ref(null)
onUpdated(() => {
nextTick(() => {
listRef.value?.scrollTo(0, listRef.value.scrollHeight)
})
})
</script>
七、易错点与注意点
7.1 mounted 里才能安全用 DOM / $refs
created(或 setup 顶层)执行时 DOM 还没有,this.$refs.xxx 或 ref(null).value 可能是 undefined。
需要操作 DOM 时,放在 mounted(或 onMounted)里;若元素是 v-if 控制的,可能还要配合 nextTick。
7.2 beforeUpdate / updated 里不要改依赖的数据
在 beforeUpdate 或 updated 里又去改导致本次更新的响应式数据,可能触发再次更新,造成死循环。
若必须改,用 nextTick 延后或加条件判断,避免循环。
7.3 unmounted 里一定要做清理
在 mounted 里加了 addEventListener、setInterval、订阅等,一定要在 unmounted 里移除/清除,否则组件销毁后这些仍然存在,会造成内存泄漏或报错。
7.4 异步请求在 unmounted 之后才返回
若在 mounted 里发了请求,组件可能在请求回来前就被销毁了。
在 unmounted 里设一个 isUnmounted 标志,在请求的回调里先判断 if (isUnmounted) return,再改数据,避免“组件已销毁还去 setData”的警告或错误。
八、生命周期钩子速查表
| 阶段 | 选项式 | 组合式 | 适合做什么 |
|---|---|---|---|
| 创建后 | created | setup 顶层 | 初始化数据、发请求(不依赖 DOM) |
| 挂载前 | beforeMount | onBeforeMount | 少用 |
| 挂载后 | mounted | onMounted | 操作 DOM、发请求、加监听 |
| 更新前 | beforeUpdate | onBeforeUpdate | 少用,勿改数据 |
| 更新后 | updated | onUpdated | DOM 更新后操作(配合 nextTick),勿改依赖数据 |
| 卸载前 | beforeUnmount | onBeforeUnmount | 少用 |
| 卸载后 | unmounted | onUnmounted | 移除监听、清定时器、做清理 |
九、学习建议
- 先记牢三个最常用:mounted(操作 DOM、发请求、加监听)、unmounted(清理)、created/setup(数据已有、DOM 未有的阶段)。
- 加了监听、定时器、订阅,一定要在 unmounted 里解绑/清除。
- 需要“DOM 更新后再做什么”时用 updated + nextTick,并避免在钩子里改导致更新的数据。
- 父子顺序:子先 mounted,父后 mounted;子先 unmounted,父后 unmounted。
把本文档里的“mounted 加监听、unmounted 移除”“mounted 开定时器、unmounted 清除”在项目里写一遍,会掌握得更牢。祝你学习顺利。