【Vue】生命周期与钩子函数

在这里插入图片描述

个人主页:Guiat
归属专栏:Vue

在这里插入图片描述

正文

1. Vue生命周期概述

Vue组件的生命周期是指组件从创建到销毁的整个过程,Vue框架在这个过程中提供了一系列的钩子函数,让开发者可以在特定的时间点添加自己的代码逻辑。理解生命周期对于正确使用Vue框架至关重要。

1.1 什么是生命周期?

生命周期是指Vue实例从创建、初始化数据、编译模板、挂载DOM、渲染、更新到销毁的一系列过程。在这个过程中,Vue实例会自动调用生命周期钩子函数,让开发者可以在不同阶段添加自己的代码。

1.2 为什么需要生命周期钩子?

生命周期钩子的主要作用包括:

  • 在合适的时机执行初始化操作
  • 在数据变化时执行特定逻辑
  • 在组件销毁前进行清理工作
  • 控制组件更新的时机和方式
  • 优化性能,避免不必要的渲染

2. Vue 2.x生命周期钩子

2.1 创建阶段

创建阶段的钩子函数在组件实例被创建时调用。

2.1.1 beforeCreate

export default {
  beforeCreate() {
    console.log('beforeCreate hook called')
    // 此时组件实例刚被创建,data和methods等选项还未初始化
    console.log('data:', this.$data) // undefined
    console.log('methods:', this.methodName) // undefined
  }
}

在这个阶段:

  • 实例已经初始化,但数据观测和事件配置都未完成
  • 无法访问data、computed、methods、watch中的数据和方法

2.1.2 created

export default {
  data() {
    return {
      message: 'Hello Vue!'
    }
  },
  methods: {
    greet() {
      return 'Greeting method'
    }
  },
  created() {
    console.log('created hook called')
    // 此时可以访问data和methods
    console.log('data:', this.message) // Hello Vue!
    console.log('method:', this.greet()) // Greeting method
    
    // 可以在这里进行API调用
    this.fetchInitialData()
  },
  methods: {
    fetchInitialData() {
      // 发起API请求获取数据
    }
  }
}

在这个阶段:

  • 实例已完成数据观测、属性和方法的运算、watch/event事件回调的配置
  • 可以访问data、computed、methods、watch中的数据和方法
  • $el属性还不可用,DOM还未挂载

2.2 挂载阶段

挂载阶段的钩子函数在组件被挂载到DOM时调用。

2.2.1 beforeMount

export default {
  beforeMount() {
    console.log('beforeMount hook called')
    // 此时模板已编译,但还未挂载到DOM
    console.log('$el:', this.$el) // 尚未挂载到DOM的元素
  }
}

在这个阶段:

  • 模板编译完成,虚拟DOM已创建
  • 即将挂载到实际DOM,但$el还不是最终的DOM元素

2.2.2 mounted

export default {
  mounted() {
    console.log('mounted hook called')
    // 此时组件已挂载到DOM
    console.log('$el:', this.$el) // 已挂载到DOM的元素
    
    // 可以在这里操作DOM
    this.$el.querySelector('.some-class').style.color = 'red'
    
    // 可以在这里初始化需要DOM的库
    this.initializeChart()
  },
  methods: {
    initializeChart() {
      // 初始化图表库,如ECharts、D3等
      const chart = echarts.init(this.$el.querySelector('#chart'))
      chart.setOption({/*...*/})
    }
  }
}

在这个阶段:

  • 组件已完全挂载到DOM
  • 可以访问和操作DOM元素
  • 适合初始化依赖DOM的库,如图表、地图等
  • 子组件的mounted也已经被调用

2.3 更新阶段

更新阶段的钩子函数在组件的数据发生变化,导致虚拟DOM重新渲染时调用。

2.3.1 beforeUpdate

export default {
  data() {
    return {
      counter: 0
    }
  },
  beforeUpdate() {
    console.log('beforeUpdate hook called')
    // 此时数据已更新,但DOM还未更新
    console.log('data:', this.counter) // 新值
    console.log('DOM:', this.$el.textContent) // 旧值
  },
  methods: {
    increment() {
      this.counter++
    }
  }
}

在这个阶段:

  • 数据已经更新
  • 虚拟DOM正在重新渲染和打补丁
  • DOM还未更新
  • 适合在更新前访问现有的DOM,如手动移除已添加的事件监听器

2.3.2 updated

export default {
  data() {
    return {
      counter: 0
    }
  },
  updated() {
    console.log('updated hook called')
    // 此时DOM已更新
    console.log('data:', this.counter) // 新值
    console.log('DOM:', this.$el.textContent) // 新值
    
    // 可以在这里操作更新后的DOM
    if (this.counter > 10) {
      this.$el.querySelector('.counter').style.color = 'red'
    }
  }
}

在这个阶段:

  • 组件DOM已经更新
  • 可以执行依赖于DOM的操作
  • 应避免在此钩子中修改组件状态,可能导致无限更新循环

2.4 销毁阶段

销毁阶段的钩子函数在组件被销毁前后调用。

2.4.1 beforeDestroy

export default {
  data() {
    return {
      timer: null
    }
  },
  created() {
    // 设置定时器
    this.timer = setInterval(() => {
      console.log('Interval running')
    }, 1000)
  },
  beforeDestroy() {
    console.log('beforeDestroy hook called')
    // 清理定时器
    clearInterval(this.timer)
    
    // 移除事件监听器
    window.removeEventListener('resize', this.handleResize)
    
    // 取消API请求
    if (this.pendingRequest) {
      this.pendingRequest.cancel()
    }
  }
}

在这个阶段:

  • 实例仍然完全可用
  • 可以访问data、methods等
  • 适合清理定时器、事件监听器、取消网络请求等

2.4.2 destroyed

export default {
  destroyed() {
    console.log('destroyed hook called')
    // 组件已完全销毁
  }
}

在这个阶段:

  • 组件实例已被销毁
  • 所有的指令已被解绑
  • 所有的事件监听器已被移除
  • 所有的子组件实例已被销毁

2.5 特殊钩子

2.5.1 activated和deactivated

这两个钩子用于处理被<keep-alive>包裹的组件。

export default {
  data() {
    return {
      loadedData: null
    }
  },
  activated() {
    console.log('Component activated')
    // 组件被激活时,可以刷新数据
    this.refreshData()
  },
  deactivated() {
    console.log('Component deactivated')
    // 组件被停用时,可以暂停一些操作
    this.pauseAnimation()
  },
  methods: {
    refreshData() {
      // 刷新数据
    },
    pauseAnimation() {
      // 暂停动画
    }
  }
}

在这些阶段:

  • activated:组件被激活时调用(被缓存的组件再次显示)
  • deactivated:组件被停用时调用(被缓存的组件隐藏)

2.5.2 errorCaptured

用于捕获来自子孙组件的错误。

export default {
  errorCaptured(err, vm, info) {
    console.log('Error captured:', err)
    console.log('Component:', vm)
    console.log('Info:', info)
    
    // 记录错误
    this.logError(err, info)
    
    // 返回false阻止错误继续向上传播
    return false
  },
  methods: {
    logError(err, info) {
      // 发送错误到日志服务
    }
  }
}

在这个钩子中:

  • 可以捕获来自子孙组件的错误
  • 可以记录错误信息
  • 可以显示错误状态
  • 返回false可以阻止错误继续向上传播

3. Vue 3.x生命周期钩子

Vue 3在保留Vue 2生命周期钩子的同时,为Composition API提供了等效的生命周期钩子函数。

3.1 选项式API生命周期钩子

Vue 3的选项式API生命周期钩子与Vue 2基本相同,但有两点变化:

  1. beforeDestroy改名为beforeUnmount
  2. destroyed改名为unmounted
export default {
  beforeCreate() {
    console.log('beforeCreate')
  },
  created() {
    console.log('created')
  },
  beforeMount() {
    console.log('beforeMount')
  },
  mounted() {
    console.log('mounted')
  },
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  updated() {
    console.log('updated')
  },
  beforeUnmount() { // 原beforeDestroy
    console.log('beforeUnmount')
  },
  unmounted() { // 原destroyed
    console.log('unmounted')
  },
  activated() {
    console.log('activated')
  },
  deactivated() {
    console.log('deactivated')
  },
  errorCaptured(err, instance, info) {
    console.log('errorCaptured')
    return false
  }
}

3.2 Composition API生命周期钩子

Vue 3的Composition API提供了一组与选项式API等效的生命周期钩子函数。

import { 
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated,
  onErrorCaptured
} from 'vue'

export default {
  setup() {
    console.log('setup - replaces beforeCreate and created')
    
    onBeforeMount(() => {
      console.log('onBeforeMount')
    })
    
    onMounted(() => {
      console.log('onMounted')
      // DOM操作
      document.querySelector('.element').style.color = 'red'
    })
    
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate')
    })
    
    onUpdated(() => {
      console.log('onUpdated')
    })
    
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount')
      // 清理工作
      clearInterval(timer)
    })
    
    onUnmounted(() => {
      console.log('onUnmounted')
    })
    
    onActivated(() => {
      console.log('onActivated')
    })
    
    onDeactivated(() => {
      console.log('onDeactivated')
    })
    
    onErrorCaptured((err, instance, info) => {
      console.log('onErrorCaptured', err)
      return false
    })
    
    // 返回要暴露给模板的内容
    return {}
  }
}

3.2.1 setup函数

setup函数是Composition API的入口点,它在组件创建过程中很早就被调用,甚至早于beforeCreate

import { ref, onMounted } from 'vue'

export default {
  props: {
    initialCount: Number
  },
  setup(props, context) {
    // props是响应式的,不能解构
    console.log(props.initialCount)
    
    // context包含emit, attrs, slots等
    const { emit, attrs, slots } = context
    
    // 创建响应式状态
    const count = ref(props.initialCount || 0)
    
    // 方法
    function increment() {
      count.value++
      emit('increment', count.value)
    }
    
    // 生命周期钩子
    onMounted(() => {
      console.log('Component mounted')
    })
    
    // 返回要暴露给模板的内容
    return {
      count,
      increment
    }
  }
}

setup函数中:

  • 无法访问this
  • 可以访问props,但它是响应式的,不应该被解构
  • 返回的对象中的属性将暴露给模板和组件实例

3.2.2 Vue 3特有的生命周期钩子

Vue 3还引入了一些新的生命周期钩子:

import { 
  onRenderTracked,
  onRenderTriggered,
  onServerPrefetch
} from 'vue'

export default {
  setup() {
    // 当组件渲染过程中追踪到响应式依赖时调用
    onRenderTracked((event) => {
      console.log('Dependency tracked:', event)
    })
    
    // 当响应式依赖触发组件重新渲染时调用
    onRenderTriggered((event) => {
      console.log('Render triggered by:', event)
    })
    
    // 服务器端渲染相关,在服务器端渲染期间获取数据
    onServerPrefetch(async () => {
      // 等待异步数据
      await fetchData()
    })
  }
}

这些钩子主要用于调试和服务器端渲染优化。

4. 生命周期钩子的实际应用

4.1 数据获取

4.1.1 在created/setup中获取数据

// Vue 2 - 选项式API
export default {
  data() {
    return {
      posts: [],
      loading: true,
      error: null
    }
  },
  created() {
    this.fetchData()
  },
  methods: {
    async fetchData() {
      try {
        this.loading = true
        const response = await fetch('https://api.example.com/posts')
        this.posts = await response.json()
      } catch (err) {
        this.error = err.message
      } finally {
        this.loading = false
      }
    }
  }
}

// Vue 3 - Composition API
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const posts = ref([])
    const loading = ref(true)
    const error = ref(null)
    
    const fetchData = async () => {
      try {
        loading.value = true
        const response = await fetch('https://api.example.com/posts')
        posts.value = await response.json()
      } catch (err) {
        error.value = err.message
      } finally {
        loading.value = false
      }
    }
    
    // 在setup中直接调用或在onMounted中调用
    fetchData()
    
    return { posts, loading, error }
  }
}

4.1.2 在路由变化时重新获取数据

// Vue 2
export default {
  data() {
    return {
      post: null,
      loading: true
    }
  },
  created() {
    this.fetchData()
  },
  watch: {
    // 监听路由参数变化
    '$route.params.id': function(newId) {
      this.fetchData()
    }
  },
  methods: {
    async fetchData() {
      this.loading = true
      const response = await fetch(`https://api.example.com/posts/${this.$route.params.id}`)
      this.post = await response.json()
      this.loading = false
    }
  }
}

// Vue 3
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    const post = ref(null)
    const loading = ref(true)
    
    const fetchData = async () => {
      loading.value = true
      const response = await fetch(`https://api.example.com/posts/${route.params.id}`)
      post.value = await response.json()
      loading.value = false
    }
    
    // 首次加载
    fetchData()
    
    // 监听路由参数变化
    watch(() => route.params.id, fetchData)
    
    return { post, loading }
  }
}

4.2 DOM操作和第三方库集成

4.2.1 在mounted中初始化第三方库

// Vue 2
export default {
  data() {
    return {
      chart: null,
      chartData: [10, 20, 30, 40, 50]
    }
  },
  mounted() {
    this.initChart()
  },
  beforeDestroy() {
    // 清理图表实例
    if (this.chart) {
      this.chart.dispose()
    }
  },
  methods: {
    initChart() {
      const chartDom = this.$refs.chartContainer
      this.chart = echarts.init(chartDom)
      this.chart.setOption({
        xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] },
        yAxis: { type: 'value' },
        series: [{ data: this.chartData, type: 'bar' }]
      })
    }
  }
}

// Vue 3
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts'

export default {
  setup() {
    const chartContainer = ref(null)
    const chart = ref(null)
    const chartData = ref([10, 20, 30, 40, 50])
    
    onMounted(() => {
      chart.value = echarts.init(chartContainer.value)
      chart.value.setOption({
        xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] },
        yAxis: { type: 'value' },
        series: [{ data: chartData.value, type: 'bar' }]
      })
    })
    
    onBeforeUnmount(() => {
      if (chart.value) {
        chart.value.dispose()
      }
    })
    
    return { chartContainer, chartData }
  }
}

4.2.2 在updated中更新第三方库

// Vue 2
export default {
  data() {
    return {
      chart: null,
      chartData: [10, 20, 30, 40, 50]
    }
  },
  mounted() {
    this.initChart()
  },
  updated() {
    // 当数据变化时更新图表
    if (this.chart) {
      this.chart.setOption({
        series: [{ data: this.chartData }]
      })
    }
  },
  methods: {
    initChart() { /* 同上 */ },
    updateData() {
      // 更新数据
      this.chartData = [30, 40, 50, 60, 70]
    }
  }
}

// Vue 3
import { ref, onMounted, onUpdated, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts'

export default {
  setup() {
    const chartContainer = ref(null)
    const chart = ref(null)
    const chartData = ref([10, 20, 30, 40, 50])
    
    onMounted(() => {
      chart.value = echarts.init(chartContainer.value)
      chart.value.setOption({
        xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] },
        yAxis: { type: 'value' },
        series: [{ data: chartData.value, type: 'bar' }]
      })
    })
    
    onUpdated(() => {
      if (chart.value) {
        chart.value.setOption({
          series: [{ data: chartData.value }]
        })
      }
    })
    
    function updateData() {
      chartData.value = [30, 40, 50, 60, 70]
    }
    
    onBeforeUnmount(() => {
      if (chart.value) {
        chart.value.dispose()
      }
    })
    
    return { chartContainer, chartData, updateData }
  }
}

4.3 资源清理

4.3.1 清理定时器和事件监听器

// Vue 2
export default {
  data() {
    return {
      timer: null,
      count: 0
    }
  },
  mounted() {
    // 设置定时器
    this.timer = setInterval(() => {
      this.count++
    }, 1000)
    
    // 添加事件监听器
    window.addEventListener('resize', this.handleResize)
  },
  beforeDestroy() {
    // 清理定时器
    clearInterval(this.timer)
    
    // 移除事件监听器
    window.removeEventListener('resize', this.handleResize)
  },
  methods: {
    handleResize() {
      console.log('Window resized')
    }
  }
}

// Vue 3
import { ref, onMounted, onBeforeUnmount } from 'vue'

export default {
  setup() {
    const count = ref(0)
    let timer = null
    
    function handleResize() {
      console.log('Window resized')
    }
    
    onMounted(() => {
      // 设置定时器
      timer = setInterval(() => {
        count.value++
      }, 1000)
      
      // 添加事件监听器
      window.addEventListener('resize', handleResize)
    })
    
    onBeforeUnmount(() => {
      // 清理定时器
      clearInterval(timer)
      
      // 移除事件监听器
      window.removeEventListener('resize', handleResize)
    })
    
    return { count }
  }
}

4.3.2 取消异步请求

// Vue 2
export default {
  data() {
    return {
      controller: null,
      data: null
    }
  },
  created() {
    this.fetchData()
  },
  beforeDestroy() {
    // 取消请求
    if (this.controller) {
      this.controller.abort()
    }
  },
  methods: {
    async fetchData() {
      // 创建AbortController
      this.controller = new AbortController()
      
      try {
        const response = await fetch('https://api.example.com/data', {
          signal: this.controller.signal
        })
        this.data = await response.json()
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Fetch aborted')
        } else {
          console.error('Fetch error:', err)
        }
      }
    }
  }
}

// Vue 3
import { ref, onMounted, onBeforeUnmount } from 'vue'

export default {
  setup() {
    const data = ref(null)
    let controller = null
    
    async function fetchData() {
      // 创建AbortController
      controller = new AbortController()
      
      try {
        const response = await fetch('https://api.example.com/data', {
          signal: controller.signal
        })
        data.value = await response.json()
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Fetch aborted')
        } else {
          console.error('Fetch error:', err)
        }
      }
    }
    
    onMounted(() => {
      fetchData()
    })
    
    onBeforeUnmount(() => {
      // 取消请求
      if (controller) {
        controller.abort()
      }
    })
    
    return { data }
  }
}

4.4 缓存组件的生命周期管理

4.4.1 使用keep-alive和相关钩子

// Vue 2
export default {
  data() {
    return {
      intervalId: null,
      time: new Date()
    }
  },
  activated() {
    // 组件被激活时启动定时器
    this.intervalId = setInterval(() => {
      this.time = new Date()
    }, 1000)
    
    // 可以刷新数据
    this.refreshData()
  },
  deactivated() {
    // 组件被停用时清除定时器
    clearInterval(this.intervalId)
  },
  methods: {
    refreshData() {
      // 刷新数据的逻辑
    }
  }
}

// Vue 3
import { ref, onActivated, onDeactivated } from 'vue'

export default {
  setup() {
    const time = ref(new Date())
    let intervalId = null
    
    onActivated(() => {
      // 组件被激活时启动定时器
      intervalId = setInterval(() => {
        time.value = new Date()
      }, 1000)
      
      // 可以刷新数据
      refreshData()
    })
    
    onDeactivated(() => {
      // 组件被停用时清除定时器
      clearInterval(intervalId)
    })
    
    function refreshData() {
      // 刷新数据的逻辑
    }
    
    return { time, refreshData }
  }
}

在父组件中使用keep-alive:

<template>
  <keep-alive>
    <component :is="currentComponent"></component>
  </keep-alive>
</template>

<script>
export default {
  data() {
    return {
      currentComponent: 'ComponentA'
    }
  }
}
</script>

5. 生命周期钩子的最佳实践

5.1 选择正确的钩子

根据需求选择合适的生命周期钩子:

需求推荐钩子
初始化非响应式变量beforeCreate
访问响应式数据created
访问DOM元素mounted
监听组件数据变化updated
清理资源beforeUnmount
处理缓存组件activated/deactivated

5.2 性能优化技巧

5.2.1 避免在updated中修改数据

// 错误示例 - 可能导致无限循环
export default {
  data() {
    return { counter: 0 }
  },
  updated() {
    // 这会触发新的更新,导致无限循环
    this.counter++
  }
}

// 正确示例 - 使用条件判断
export default {
  data() {
    return { 
      counter: 0,
      needsUpdate: true
    }
  },
  updated() {
    if (this.needsUpdate) {
      this.needsUpdate = false
      this.performOneTimeUpdate()
    }
  },
  methods: {
    performOneTimeUpdate() {
      // 执行一次性更新
    }
  }
}

5.2.2 使用计算属性代替updated钩子

// 不推荐 - 在updated中计算派生值
export default {
  data() {
    return {
      items: [1, 2, 3],
      total: 0
    }
  },
  updated() {
    this.total = this.items.reduce((sum, item) => sum + item, 0)
  }
}

// 推荐 - 使用计算属性
export default {
  data() {
    return {
      items: [1, 2, 3]
    }
  },
  computed: {
    total() {
      return this.items.reduce((sum, item) => sum + item, 0)
    }
  }
}

5.2.3 延迟执行非关键操作

export default {
  mounted() {
    // 立即执行关键操作
    this.initCriticalFeatures()
    
    // 延迟执行非关键操作
    this.$nextTick(() => {
      this.initNonCriticalFeatures()
    })
    
    // 或使用setTimeout进一步延迟
    setTimeout(() => {
      this.initDeferredFeatures()
    }, 100)
  }
}

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guiat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值