Code前端首页关于Code前端联系我们

Vue2生命周期有哪些核心阶段?

terry 6小时前 阅读数 6 #Vue
文章标签 Vue2;生命周期

p>咱做Vue2项目的时候,经常得处理组件从创建、渲染到销毁的各种逻辑,比如啥时候发请求、啥时候操作DOM、啥时候清理定时器…这时候就得搞懂生命周期!可Vue2的生命周期到底有多少阶段?每个阶段能干啥、不能干啥?父子组件之间生命周期咋配合?今天就把这些问题一个个拆明白,哪怕是刚学Vue的新手也能看懂~

Vue2的生命周期可以分成创建、挂载、更新、销毁四大阶段,每个阶段对应不同的“钩子函数”(就是Vue自动调用的函数,你可以在里面写逻辑),咱一个个拆:

创建阶段:beforeCreate → created

  • beforeCreate:组件实例刚初始化(比如new Vue()之后),但数据观测(data里的变量还没变成响应式)、事件配置(methods里的方法还不能用)都没完成,这时候this.data拿不到,this.methods也调不了,实际开发中很少用,顶多做些“最最早期”的初始化(比如框架级别的埋点配置,但场景极少见)。
  • created:组件实例创建完成!这时候数据是响应式的(data能访问)、方法也能调用(methods可用),但DOM还没渲染($elundefined,页面上看不到真实元素),这阶段最适合发Ajax请求——因为数据和方法都准备好了,请求回来的数据能直接存到data里,等DOM渲染时就能用。

挂载阶段:beforeMount → mounted

  • beforeMount:Vue开始编译模板(把<template>里的代码转成虚拟DOM),但真实DOM还没挂载到页面,这时候$el是“虚拟DOM”(能拿到,但不是页面上的真实元素),操作DOM没用(因为页面上还没有),实际用得少,偶尔用来做“模板编译前的最后调整”(比如动态改模板里的某个配置,但场景不多)。
  • mounted真实DOM已经挂载到页面!这时候$el是页面上的真实元素,this.$refs也能拿到DOM节点了。操作DOM、初始化第三方库(比如轮播图、富文本编辑器)都得在这阶段做——因为只有真实DOM存在,第三方库才能找到容器渲染。

更新阶段:beforeUpdate → updated

组件里的响应式数据变化后(比如data里的变量被修改),就会触发更新阶段:

  • beforeUpdate:数据已经变了,但DOM还没更新(页面上显示的还是旧数据),这时候能拿到“更新前的数据”,可以做一些判断(比如对比新旧数据,决定是否要拦截更新)。
  • updatedDOM已经跟着数据更新完了,页面上显示的是新内容,这时候可以基于新DOM做操作(比如重新计算表格高度、调整动画)。

销毁阶段:beforeDestroy → destroyed

当组件被销毁时(比如用v-if把组件从页面移除),会触发这两个钩子:

  • beforeDestroy:组件实例还没销毁,数据、方法、DOM都还能访问,这阶段必须做“资源清理”——比如清除定时器(clearInterval)、移除事件监听(removeEventListener),不然组件销毁后这些东西还在运行,会导致内存泄漏。
  • destroyed:组件实例彻底销毁,所有数据绑定、事件监听、子组件都被清理,这时候基本没什么可操作的了,因为实例已经“拆干净”了,顶多做些最终的日志上报。

每个生命周期钩子适合做啥实际操作?

光知道阶段还不够,得结合业务场景用对地方,咱用“做一个博客详情页”“实现轮播图”这些例子,看看每个钩子咋用:

created:发Ajax请求,初始化数据

比如做博客详情页,需要先请求文章数据:

export default {
  data() {
    return { article: {} }
  },
  created() {
    // 这里发请求,拿到数据后存到data
    this.fetchArticle() 
  },
  methods: {
    async fetchArticle() {
      const res = await axios.get('/api/article/123')
      this.article = res.data
    }
  }
}

为啥不在mounted发请求?因为created更早,请求在DOM渲染前就发起,能减少首屏等待时间(用户感觉加载更快)。

mounted:操作真实DOM,初始化第三方库

比如做轮播图,需要依赖真实DOM容器:

export default {
  mounted() {
    // 初始化swiper轮播图,this.$refs.slider是真实DOM
    new Swiper(this.$refs.slider, {
      loop: true
    })
  }
}

如果在beforeMount里初始化,this.$refs.slider还是虚拟DOM,Swiper找不到真实容器,就会报错。

beforeUpdate:数据更新前,做“拦截”或“记录”

比如购物车数量变化时,想在DOM更新前记录旧数量:

export default {
  data() {
    return { cartCount: 0 }
  },
  beforeUpdate() {
    // 记录更新前的数量
    this.oldCount = this.cartCount 
  },
  updated() {
    // DOM更新后,对比新旧数量,提示用户
    if (this.cartCount > this.oldCount) {
      alert('新增了' + (this.cartCount - this.oldCount) + '件商品')
    }
  }
}

beforeDestroy:清理定时器、事件监听

比如组件里有定时请求,必须在销毁前清除:

export default {
  data() {
    return { timer: null }
  },
  created() {
    this.timer = setInterval(() => {
      this.fetchNewMsg()
    }, 5000)
  },
  beforeDestroy() {
    // 销毁前清除定时器,否则组件没了,定时器还在跑
    clearInterval(this.timer) 
  }
}

父子组件生命周期执行顺序是啥逻辑?

实际项目里组件都是嵌套的(父组件里包着子组件),这时候生命周期执行顺序很关键!分**加载、更新、销毁**三种场景:

加载阶段(父组件渲染子组件)

执行顺序是:
beforeCreate → 父created → 父beforeMount → 子beforeCreate → 子created → 子beforeMount → 子mounted → 父mounted

逻辑:父组件先完成自己的“创建阶段”,然后准备挂载(beforeMount),这时候要渲染子组件,所以子组件开始自己的“创建→挂载”流程,等子组件挂载完成(mounted),父组件才会完成自己的挂载(mounted)。

更新阶段(父/子组件数据变化)

假设父组件数据变化,导致子组件的props变化,执行顺序是:
beforeUpdate → 子beforeUpdate → 子updated → 父updated

逻辑:父组件数据变了,先进入自己的beforeUpdate,因为子组件的props依赖父组件数据,所以子组件也会触发更新,进入beforeUpdate,等子组件完成DOM更新(updated),父组件才会完成自己的DOM更新(updated)。

销毁阶段(父组件销毁,子组件也被销毁)

执行顺序是:
beforeDestroy → 子beforeDestroy → 子destroyed → 父destroyed

逻辑:父组件要销毁时,先进入自己的beforeDestroy,然后通知子组件“你也得销毁”,子组件先执行beforeDestroy(清理自己的资源),再执行destroyed(彻底销毁),最后父组件执行destroyed,完成整个销毁流程。

举个代码例子(看控制台输出顺序更直观):
父组件里引入子组件,分别在每个钩子里console.log,加载时输出顺序就是上面说的“父创建→子创建→子挂载→父挂载”,更新时修改父组件数据,输出顺序是“父beforeUpdate→子beforeUpdate→子updated→父updated”,销毁时用v-if把父组件隐藏,输出顺序是“父beforeDestroy→子beforeDestroy→子destroyed→父destroyed”。

开发中利用生命周期优化性能有啥技巧?

懂了生命周期,就能在合适的时机做合适的事,减少性能浪费:

数据请求时机:用created而不是mounted

createdmounted早执行,请求发起得更早,能减少首屏加载时间,比如首页列表数据,在created里发请求,数据回来后mounted阶段DOM才渲染,用户感觉“加载快”。

延迟加载非必要资源:mounted里做“条件加载”

有些资源(比如图表库、地图SDK)不是页面一加载就需要的,可以在mounted里判断“用户是否滚动到该组件”,再加载资源。

mounted() {
  // 监听页面滚动,判断组件是否进入视口
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      // 进入视口后,才加载ECharts
      import('echarts').then((echarts) => {
        this.initChart(echarts)
      })
      observer.unobserve(this.$el) // 只加载一次
    }
  })
  observer.observe(this.$el)
}

及时清理资源:beforeDestroy里做“善后”

组件销毁后,定时器、事件监听、WebSocket连接这些如果不清理,会一直占用内存(内存泄漏),所以必须在beforeDestroy里做:

beforeDestroy() {
  // 清除定时器
  clearInterval(this.timer)
  // 移除事件监听
  window.removeEventListener('resize', this.handleResize)
  // 关闭WebSocket
  this.ws.close()
}

避免不必要的更新:beforeUpdate里做“防抖/节流”

如果数据变化太频繁(比如输入框实时搜索),会导致DOM频繁更新,性能变差,可以在beforeUpdate里判断“数据变化间隔是否足够大”,再决定是否更新:

data() {
  return { 
    searchValue: '',
    lastUpdateTime: Date.now()
  }
},
beforeUpdate() {
  const now = Date.now()
  // 间隔小于500ms,就阻止这次更新(节流)
  if (now - this.lastUpdateTime < 500) {
    // 阻止更新(需要结合Vue的更新机制,比如用标志位)
    this.shouldUpdate = false 
  } else {
    this.shouldUpdate = true
    this.lastUpdateTime = now
  }
},
// 配合计算属性或watch,根据shouldUpdate决定是否渲染

生命周期常见的坑和避坑方法是啥?

刚学的时候,很容易因为“时机不对”踩坑,这几个典型错误得注意:

坑1:beforeMount里操作DOM,拿不到元素

场景:想在组件加载时给按钮加点击事件,用document.getElementById('btn'),但beforeMount里执行,控制台报错“null”。
原因beforeMount阶段真实DOM还没挂载到页面,getElementById找不到元素。
解决:把DOM操作移到mounted里。

坑2:created里用this.$refs,拿不到子组件

场景:父组件想在created里调用子组件的方法,写this.$refs.child.method(),结果报错“method is not a function”。
原因created阶段DOM还没渲染,$refs是基于DOM的,所以子组件的$refs还没生成。
解决:要么把逻辑移到mounted,要么用this.$nextTick(等DOM更新后再执行):

created() {
  this.$nextTick(() => {
    this.$refs.child.method() // DOM渲染后,$refs才有值
  })
}

坑3:销毁阶段没清理定时器,内存泄漏

场景:组件里有setInterval,切换页面后定时器还在跑,控制台一直报错“找不到DOM”。
原因:组件销毁后,定时器没被清除,还在执行回调函数。
解决:在beforeDestroyclearInterval

data() {
  return { timer: null }
},
created() {
  this.timer = setInterval(() => { ... }, 1000)
},
beforeDestroy() {
  clearInterval(this.timer)
}

坑4:误解mounted只执行一次

场景:组件用v-if控制显示隐藏,每次显示时mounted里的初始化逻辑会重复执行(比如重复初始化地图,导致性能变差)。
原因v-if销毁组件后,再次显示会重新创建组件实例,所以mounted会再执行。
解决:加个“是否已初始化”的标志位:

data() {
  return { isMapInit: false }
},
mounted() {
  if (!this.isMapInit) {
    this.initMap() // 只初始化一次
    this.isMapInit = true
  }
}

实际项目案例:用生命周期做TodoList

光讲理论太虚,咱用“TodoList”这个经典需求,把生命周期串起来:

created:请求待办数据

组件创建后,立刻请求后端的待办列表:

created() {
  this.fetchTodos() // 发请求,把数据存到this.todos
}

mounted:让输入框自动聚焦

页面渲染后,让输入框自动获得焦点(提升用户体验):

mounted() {
  this.$refs.input.focus() // $refs.input是输入框的DOM
}

beforeUpdate:记录更新前的待办数量

用户添加/删除待办时,记录更新前的数量,方便后续提示:

data() {
  return { todos: [], oldCount: 0 }
},
beforeUpdate() {
  this.oldCount = this.todos.length // 记录更新前的数量
}

updated:提示待办数量变化

DOM更新后,对比新旧数量,给用户反馈:

updated() {
  const newCount = this.todos.length
  if (newCount > this.oldCount) {
    alert('新增了' + (newCount - this.oldCount) + '条待办')
  } else if (newCount < this.oldCount) {
    alert('完成了' + (this.oldCount - newCount) + '条待办')
  }
}

beforeDestroy:清理键盘事件监听

组件销毁前,移除全局的键盘事件(比如按Enter键添加待办的监听):

created() {
  window.addEventListener('keydown', this.handleKeydown)
},
beforeDestroy() {
  window.removeEventListener('keydown', this.handleKeydown)
}

通过这个案例,能直观看到每个生命周期钩子在业务中的作用——从数据请求、DOM操作到资源清理,全流程覆盖。

Vue2的生命周期就像组件的“成长日记”:从创建时的懵懂(beforeCreate)、拿到数据的兴奋(created)、挂载到页面的成熟(mounted)、数据变化时的迭代(updated),到最终销毁时的善后(beforeDestroy)…每个阶段都有明确的分工,搞懂这些,写组件时就知道“啥时候该干啥”,既能避免BUG,又能优化性能,下次写Vue组件,不妨先想想:这个逻辑该放在哪个生命周期里?

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门