Vue3 生命周期完全指南:从出生到消亡的组件旅程

322 阅读8分钟

前言:什么是生命周期?

想象一下一个人的一生:出生 → 成长 → 工作 → 退休 → 离世。Vue 组件也有类似的生命周期,从创建到销毁的整个过程就是生命周期。

生命周期钩子就是 Vue 在组件生命周期的不同阶段自动调用的函数,让我们有机会在特定时刻执行自己的代码。

第一章:生命周期全景图

生命周期全景图来源于vue官网.png

概括来总结一下生命周期:

组件诞生
    ↓
beforeCreate    → 组件刚初始化,什么都还没有
    ↓  
created         → 数据已初始化,但DOM还未创建
    ↓
beforeMount     → 模板编译完成,但还未挂载到页面
    ↓
mounted         → 组件已显示在页面中 ✅
    ↓
    ┌─→ beforeUpdate  → 数据变化,DOM更新前
    │     ↓
    │   updated      → DOM已更新完成
    │     ↓
    └─────┘
    ↓
beforeUnmount   → 组件即将被销毁
    ↓
unmounted       → 组件已销毁 🏁

在这个过程中,Vue 会在不同阶段自动触发一些“钩子函数”(生命周期钩子),我们就可以在这些关键节点里插入自己的逻辑。

📌 打个比方:

  • beforeCreate 就像刚出生的婴儿,什么都还没有;

  • created 就像长大拥有了思想,但还看不见世界;

  • mounted 就像登上舞台,终于亮相;

  • updated 就像换上新衣,焕然一新;

  • unmounted 就是彻底谢幕,离开舞台。

第二章:详细解析每个生命周期阶段

2.1 创建阶段(组件诞生)

<template>
  <div class="lifecycle-demo">
    <h3>{{ title }}</h3>
    <p>计数: {{ count }}</p>
    <button @click="count++">增加</button>
    <div ref="messageElement">我是DOM元素</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: '生命周期演示',
      count: 0
    }
  },
  
  // 1. beforeCreate - 组件刚初始化
  beforeCreate() {
    console.log('🚀 beforeCreate 阶段')
    console.log('数据:', this.title)        // undefined - 数据还未初始化
    console.log('DOM:', this.$refs.messageElement) // undefined - DOM不存在
    console.log('-------------------')
  },
  
  // 2. created - 数据已准备好
  created() {
    console.log('✅ created 阶段')
    console.log('数据:', this.title)        // '生命周期演示' - 数据已初始化
    console.log('DOM:', this.$refs.messageElement) // undefined - DOM还未创建
    console.log('可以在这里调用API获取数据')
    console.log('-------------------')
    
    // 常用场景:初始化数据、调用API
    this.fetchData()
  },
  
  methods: {
    fetchData() {
      // 模拟API调用
      console.log('正在从服务器获取数据...')
    }
  }
}
</script>

2.2 挂载阶段(显示到页面)

<script>
export default {
  // ... 之前的代码
  
  // 3. beforeMount - 模板编译完成,即将显示
  beforeMount() {
    console.log('📝 beforeMount 阶段')
    console.log('DOM:', this.$refs.messageElement) // undefined - 还未插入页面
    console.log('模板已编译,但还未挂载到DOM中')
    console.log('-------------------')
  },
  
  // 4. mounted - 组件已显示在页面
  mounted() {
    console.log('🎉 mounted 阶段')
    console.log('DOM:', this.$refs.messageElement) // 真实的DOM元素
    console.log('组件已显示在页面中,可以操作DOM了')
    console.log('-------------------')
    
    // 常用场景:操作DOM、初始化第三方库、添加事件监听
    this.initThirdPartyLibrary()
    this.startTimer()
  },
  
  methods: {
    initThirdPartyLibrary() {
      // 比如初始化图表库、地图等
      console.log('初始化ECharts图表...')
    },
    
    startTimer() {
      // 启动定时器
      this.timer = setInterval(() => {
        console.log('定时器运行中...')
      }, 1000)
    }
  }
}
</script>

2.3 更新阶段(数据变化时)

<script>
export default {
  // ... 之前的代码
  
  // 5. beforeUpdate - 数据变化,但DOM还未更新
  beforeUpdate() {
    console.log('🔄 beforeUpdate 阶段')
    console.log('数据已变化,新count:', this.count)
    console.log('但DOM还未更新,页面显示的还是旧值')
    console.log('-------------------')
  },
  
  // 6. updated - DOM已更新完成
  updated() {
    console.log('✨ updated 阶段')
    console.log('DOM已更新完成,页面显示新值')
    console.log('可以在这里获取更新后的DOM状态')
    console.log('-------------------')
    
    // 注意:不要在这里修改数据,可能导致无限循环!
    // this.count++  // ❌ 危险操作!
  }
}
</script>

2.4 销毁阶段(组件消亡)

<script>
export default {
  // ... 之前的代码
  
  // 7. beforeUnmount - 组件即将被销毁
  beforeUnmount() {
    console.log('🛑 beforeUnmount 阶段')
    console.log('组件即将被销毁,但还可以访问数据和方法')
    console.log('这是清理工作的最后机会')
    console.log('-------------------')
    
    // 常用场景:清除定时器、取消网络请求、移除事件监听
    this.cleanup()
  },
  
  // 8. unmounted - 组件已销毁
  unmounted() {
    console.log('💀 unmounted 阶段')
    console.log('组件已完全销毁,所有东西都被清理了')
    console.log('-------------------')
  },
  
  methods: {
    cleanup() {
      // 清除定时器
      if (this.timer) {
        clearInterval(this.timer)
        console.log('定时器已清除')
      }
      
      // 取消网络请求
      console.log('取消未完成的网络请求...')
    }
  }
}
</script>

第三章:组合式API中的生命周期

在 Vue3 的组合式 API 中,我们不再写 beforeCreate、mounted 这样的选项式,而是通过 在 setup() 中调用函数 来注册生命周期钩子。

⚠️ 注意: 所有生命周期 API 必须 在 setup() 阶段同步调用,不能放在异步回调里。


1. onBeforeMount

时机:组件完成响应式状态设置,但 DOM 还没生成。即将执行首次渲染。

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

onBeforeMount(() => {
  console.log('📝 组件即将挂载,还没有DOM')
})
</script>

📌 常见用途:在渲染前做一些最后准备,比如打印日志、检查数据。


2. onMounted

时机:组件挂载到 DOM 后执行。此时可以安全操作 DOM 或使用第三方库。

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

const el = ref(null)

onMounted(() => {
  console.log('🎉 组件挂载完成,可以访问DOM:', el.value)
})
</script>

<template>
  <div ref="el">Hello</div>
</template>

📌 常见用途:初始化图表、地图、事件监听。


3. onBeforeUpdate

时机:组件因为响应式数据变化而更新 DOM 前调用。此时数据是新的,但 DOM 还是旧的。

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

const count = ref(0)

onBeforeUpdate(() => {
  console.log('🔄 即将更新,旧的DOM还在,新的count:', count.value)
})
</script>

📌 常见用途:在更新前读取旧 DOM 状态。


4. onUpdated

时机:组件 DOM 更新完成后调用。

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

const count = ref(0)

onUpdated(() => {
  console.log('✨ DOM已更新,现在按钮内容是:', document.getElementById('btn').textContent)
})
</script>

<template>
  <button id="btn" @click="count++">{{ count }}</button>
</template>

⚠️ 注意:不要在 onUpdated 中直接改数据,否则可能造成无限循环。

📌 常见用途:依赖更新后的 DOM 做副作用,比如滚动定位。


5. onBeforeUnmount

时机:组件即将卸载,但功能还在。可以做一些清理准备。

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

onBeforeUnmount(() => {
  console.log('🛑 组件即将卸载')
})
</script>

6. onUnmounted

时机:组件彻底卸载完成,所有响应式作用都停掉。

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

let timer
onMounted(() => {
  timer = setInterval(() => console.log('⏰ 计时器运行中'), 1000)
})

onUnmounted(() => {
  clearInterval(timer)
  console.log('💀 已清理资源')
})
</script>

📌 常见用途:清理定时器、事件监听、网络连接。


7. onErrorCaptured

时机:捕获到子组件的错误时触发。

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

onErrorCaptured((err, instance, info) => {
  console.error('❌ 捕获到错误:', err, '来源:', info)
  return false // 阻止继续向上传递
})
</script>

📌 常见用途:集中处理错误,比如显示错误 UI。


8. onRenderTracked / onRenderTriggered

仅开发模式可用,用于调试响应式依赖。

<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked(e => {
  console.log('👀 追踪到依赖:', e)
})

onRenderTriggered(e => {
  console.log('⚡ 渲染触发原因:', e)
})
</script>

📌 常见用途:性能调优,分析组件的渲染依赖。


9. onActivated / onDeactivated

时机:当组件被 缓存时:

  • onActivated → 组件被激活
  • onDeactivated → 组件被移出 DOM 但缓存还在
<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  console.log('🔵 组件激活')
})

onDeactivated(() => {
  console.log('⚪ 组件失活')
})
</script>

📌 常见用途:缓存组件时恢复/暂停功能(比如视频播放)。


10. onServerPrefetch

仅在 SSR(服务端渲染)中可用:在组件渲染前预取数据。

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

const data = ref(null)

onServerPrefetch(async () => {
  data.value = await fetchOnServer()
})

onMounted(async () => {
  if (!data.value) {
    data.value = await fetchOnClient()
  }
})
</script>

📌 常见用途:SSR 预取数据,避免白屏。


小结

组合式 API 的生命周期钩子基本覆盖了选项式的所有功能,还增加了调试(onRenderTracked)、缓存组件(onActivated)和 SSR(onServerPrefetch)相关钩子。

👉 记忆方式:

  • 挂载前后 → onBeforeMount / onMounted
  • 更新前后 → onBeforeUpdate / onUpdated
  • 卸载前后 → onBeforeUnmount / onUnmounted
  • 调试 & 错误 → onErrorCaptured / onRenderTracked / onRenderTriggered
  • 缓存组件 → onActivated / onDeactivated
  • SSR → onServerPrefetch

第四章:实际应用场景

4.1 数据获取场景

<template>
  <div class="user-profile">
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">加载失败: {{ error }}</div>
    <div v-else>
      <h3>{{ user.name }}</h3>
      <p>邮箱: {{ user.email }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: null,
      loading: false,
      error: null
    }
  },
  
  async created() {
    // 在created中获取数据,这样组件一创建就开始请求
    await this.fetchUserData()
  },
  
  methods: {
    async fetchUserData() {
      this.loading = true
      this.error = null
      
      try {
        // 模拟API调用
        const response = await this.$http.get('/api/user/1')
        this.user = response.data
      } catch (err) {
        this.error = err.message
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

4.2 第三方库集成

<template>
  <div ref="chartContainer" style="width: 600px; height: 400px;"></div>
</template>

<script>
import * as echarts from 'echarts'

export default {
  data() {
    return {
      chart: null
    }
  },
  
  mounted() {
    // 在mounted中初始化图表,因为这时DOM已经存在
    this.initChart()
  },
  
  beforeUnmount() {
    // 组件销毁前清理图表实例,避免内存泄漏
    if (this.chart) {
      this.chart.dispose()
    }
  },
  
  methods: {
    initChart() {
      this.chart = echarts.init(this.$refs.chartContainer)
      
      const option = {
        title: { text: '销售图表' },
        xAxis: { data: ['1月', '2月', '3月'] },
        yAxis: {},
        series: [{ type: 'bar', data: [100, 200, 150] }]
      }
      
      this.chart.setOption(option)
    }
  }
}
</script>

4.3 事件监听管理

<template>
  <div class="resize-demo">
    <p>窗口宽度: {{ windowWidth }}px</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      windowWidth: 0
    }
  },
  
  mounted() {
    // 添加事件监听
    this.windowWidth = window.innerWidth
    window.addEventListener('resize', this.handleResize)
  },
  
  beforeUnmount() {
    // 移除事件监听,避免内存泄漏
    window.removeEventListener('resize', this.handleResize)
  },
  
  methods: {
    handleResize() {
      this.windowWidth = window.innerWidth
      console.log('窗口大小变化:', this.windowWidth)
    }
  }
}
</script>

第五章:常见问题和最佳实践

5.1 生命周期执行顺序

<!-- ParentComponent.vue -->
<template>
  <ChildComponent />
</template>

<script>
export default {
  beforeCreate() { console.log('父组件 beforeCreate') },
  created() { console.log('父组件 created') },
  beforeMount() { console.log('父组件 beforeMount') },
  mounted() { console.log('父组件 mounted') }
}
</script>

<!-- ChildComponent.vue -->
<script>
export default {
  beforeCreate() { console.log('子组件 beforeCreate') },
  created() { console.log('子组件 created') },
  beforeMount() { console.log('子组件 beforeMount') },
  mounted() { console.log('子组件 mounted') }
}
</script>

执行顺序:

父组件 beforeCreate
父组件 created  
父组件 beforeMount
  子组件 beforeCreate
  子组件 created
  子组件 beforeMount
  子组件 mounted
父组件 mounted

5.2 避免的陷阱

<script>
export default {
  data() {
    return {
      items: []
    }
  },
  
  // ❌ 错误:在beforeCreate中访问数据
  beforeCreate() {
    console.log(this.items) // undefined
  },
  
  // ❌ 错误:在created中操作DOM
  created() {
    document.querySelector('.my-element') // null 或 错误的元素
  },
  
  // ❌ 错误:在updated中修改数据(可能导致无限循环)
  updated() {
    if (this.items.length > 10) {
      this.items.pop() // 危险!
    }
  },
  
  // ✅ 正确:在created中初始化数据
  created() {
    this.fetchData()
  },
  
  // ✅ 正确:在mounted中操作DOM
  mounted() {
    this.$refs.myElement.focus()
  },
  
  // ✅ 正确:使用计算属性或侦听器替代updated中的数据修改
  watch: {
    items: {
      handler(newItems) {
        if (newItems.length > 10) {
          this.items = newItems.slice(0, 10)
        }
      },
      deep: true
    }
  }
}
</script>

第六章:生命周期记忆口诀

为了方便记忆,可以记住这个口诀:

"创挂更销"四大阶段

创(创建阶段)
  beforeCreate - 初生婴儿,什么都没有
  created      - 有了思想,但看不见世界

挂(挂载阶段)  
  beforeMount  - 准备好亮相,但还在后台
  mounted      - 登台表演,观众可见

更(更新阶段)
  beforeUpdate - 准备换装,旧装还在身
  updated      - 新装亮相,焕然一新

销(销毁阶段)
  beforeUnmount - 准备谢幕,还能说话
  unmounted     - 完全退场,清理舞台

总结

通过本文的学习,你应该已经掌握了:

  1. 生命周期概念 - 组件从创建到销毁的完整过程
  2. 8个生命周期钩子 - 每个阶段的作用和时机
  3. 组合式API用法 - 新的生命周期函数使用方式
  4. 实际应用场景 - 数据获取、第三方库集成等
  5. 最佳实践 - 避免常见陷阱,写出更好的代码

记住生命周期的重要性:合理使用生命周期钩子可以让你的组件更加健壮,避免内存泄漏,提升用户体验。