Vue3中生命周期钩子

Vue 3 生命周期钩子完全指南

本文档从零开始讲解 Vue 3 的生命周期钩子:组件从创建到销毁会经历哪些阶段、每个阶段对应哪些钩子、在钩子里能做什么和不能做什么、选项式 API 与组合式 API 的写法对比,以及常见用法示例,适合新手系统学习。


一、什么是“生命周期”?

1.1 生活中的类比

一个组件就像一个人:出生(创建)→ 出现在世界上(挂载到页面)→ 成长变化(数据更新、视图更新)→ 离开(从页面移除、销毁)。
Vue 在这些关键时间点提供了“钩子”(函数),你可以在里面写自己的代码,例如:组件挂载好后去请求接口、即将销毁前清除定时器。

1.2 为什么需要钩子?

  • created / setup 执行完:数据、计算属性已经有了,但还没有 DOM,不能操作 document.querySelector。
  • mounted:组件已经挂载到 DOM,可以安全地操作自己的 DOM、发请求、加事件监听。
  • unmounted:组件即将被移除,应在这里移除监听、清定时器,避免内存泄漏。

所以“生命周期”就是组件的几个阶段,“钩子”就是你在每个阶段可以插入的代码


二、生命周期有哪些阶段?(总览)

Vue 3 组件的生命周期大致分为四段:

  1. 创建(Creation):组件实例被创建,数据、计算属性等初始化。
  2. 挂载(Mounting):把组件渲染出来的 DOM 插入到页面上。
  3. 更新(Updating):响应式数据变化,组件重新渲染。
  4. 卸载(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.querySelectorthis.$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.xxxref(null).value 可能是 undefined。
需要操作 DOM 时,放在 mounted(或 onMounted)里;若元素是 v-if 控制的,可能还要配合 nextTick

7.2 beforeUpdate / updated 里不要改依赖的数据

beforeUpdateupdated 里又去改导致本次更新的响应式数据,可能触发再次更新,造成死循环。
若必须改,用 nextTick 延后或加条件判断,避免循环。

7.3 unmounted 里一定要做清理

mounted 里加了 addEventListenersetInterval订阅等,一定要在 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 移除监听、清定时器、做清理

九、学习建议

  1. 先记牢三个最常用mounted(操作 DOM、发请求、加监听)、unmounted(清理)、created/setup(数据已有、DOM 未有的阶段)。
  2. 加了监听、定时器、订阅,一定要在 unmounted 里解绑/清除
  3. 需要“DOM 更新后再做什么”时用 updated + nextTick,并避免在钩子里改导致更新的数据。
  4. 父子顺序:子先 mounted,父后 mounted;子先 unmounted,父后 unmounted。

把本文档里的“mounted 加监听、unmounted 移除”“mounted 开定时器、unmounted 清除”在项目里写一遍,会掌握得更牢。祝你学习顺利。

发表评论