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

Vue3里还有beforeDestroy生命周期钩子吗?什么时候用?怎么正确用?

terry 50分钟前 阅读数 13 #Vue

Vue3官方文档里提过beforeDestroy吗?它的存在状态到底是啥

很多刚从Vue2转Vue3的人,或者习惯直接搜“Vue3常用钩子”找清单的朋友,可能会遇到个小困惑:翻Composition API的教程好像没怎么提它,翻Options API的全生命周期钩子列表,官方也标了个不太起眼的状态,先明确一个核心结论——Vue3里Options API依然保留了beforeDestroy钩子,但它是“向后兼容的别名”,更推荐用语义化更清晰的新钩子;而Composition API里,从一开始就没给beforeDestroy留位置,直接用对应的替代函数就行。

那为什么官方要做这种处理呢?得从Vue2到Vue3的底层变化和语义优化说起,Vue2里,beforeDestroy和beforeUnmount其实是完全同步触发、功能完全一致的钩子,只是因为最早的命名没太贴合实际场景的语义——beforeDestroy说的是“实例即将销毁”,但Vue2和Vue3的“销毁”其实是两步走:第一步是“卸载”(unmount),也就是把组件从DOM树里拆下来,解绑事件监听器、watchers这些;第二步才是真正的“内存销毁”(destroy),但内存销毁大部分时候是由浏览器的垃圾回收机制自动完成的,开发者几乎干预不了,也没必要在这个阶段做额外操作,所以Vue3为了让命名更“说人话”,官方主打的、语义更准确的钩子是beforeUnmount和unmounted,而把beforeDestroy和destroyed当成旧版名字留着,给还在大规模迁移Vue2老项目的团队一个缓冲期,但新开发的代码,不管用Options还是Composition,都强烈建议弃用旧名。

如果是Vue2转Vue3的老项目,beforeDestroy能直接用吗?有没有什么坑

既然是向后兼容的别名,理论上直接复制粘贴Vue2里beforeDestroy的代码到Vue3的Options API里是没问题的,不会报错,触发时机也和Vue2一模一样——都是在组件从DOM树中被完全移除之前执行,这时候组件的data、methods、computed这些东西都还在,也能正常访问DOM元素(虽然马上就用不上了),但这里有个小细节需要注意:虽然Vue3官方文档没说,但在某些极端情况下,比如组件是被<keep-alive>包裹的,然后通过切换<keep-alive>的include/exclude数组把组件强制移除,beforeDestroy和beforeUnmount的行为在不同版本的Vue3(比如3.0.x早期版本和3.3.x之后的版本)会不会有细微差异?这个我之前帮朋友排查过一个keep-alive里echarts未完全销毁的小bug,最后发现不管是旧名还是新名,3.3.x之后的触发逻辑都是统一的,但为了稳妥起见,既然有官方推荐的新钩子,老项目在做小版本迭代或者局部重构的时候,最好顺手把beforeDestroy/destroyed换成beforeUnmount/unmounted,避免以后Vue4(如果有)彻底砍掉旧名的时候出问题,也方便团队里习惯Vue3新写法的人维护。

那Composition API里用旧钩子的话会怎么样?比如你在setup函数里写onBeforeDestroy()?放心,Vue3的编译器会直接报错,告诉你“onBeforeDestroy is not defined”,因为Composition API从设计之初就只提供了和新语义对应的组合式函数:onBeforeUnmount、onUnmounted,完全没有兼容旧名的组合式函数,所以Composition API里根本不存在用beforeDestroy的可能性,只能乖乖用新的。

Vue3里不管用旧名还是新名,beforeUnmount(对应beforeDestroy)到底什么时候用?这三个高频场景最实用

很多开发者刚接触生命周期钩子的时候,只会在mounted里放初始化代码,updated里放更新后的,然后beforeUnmount/destroyed里随便写两句console.log试试水,根本不知道什么时候真的需要用它,其实这个钩子的核心作用就一个:做组件卸载前的“收尾工作”,清理掉那些Vue框架不会自动帮你清理的东西,防止内存泄漏或者其他奇怪的bug,Vue框架会自动清理的东西包括:组件内部注册的原生DOM事件监听器(比如用@click或者this.$el.addEventListener?不对,等下,原生addEventListener绑定的如果没有手动清理,框架是不会管的!这点很多人会踩坑)、组件内部的watchers(不管是用Options的watch还是Composition的watch/watchEffect,只要没有手动stop的话,组件卸载时会自动停止)、组件内部注册的子组件实例、组件的响应式数据(虽然数据还在,但垃圾回收机制很快会收走),那框架不管的、需要你手动在beforeUnmount里清理的东西,就是我们这个钩子的主要使用场景,我总结了三个平时开发中遇到最多的:

清理组件内部手动绑定的全局/非绑定在组件根元素上的原生DOM事件监听器

这个是最常见的内存泄漏原因之一,举个例子,你做了一个悬浮窗组件,需要监听整个window的scroll事件来调整悬浮窗的位置,这时候你肯定不能用组件根元素的@scroll,因为scroll事件是绑定在window上的,那如果你在mounted里写了window.addEventListener('scroll', this.handleScroll),但不在beforeUnmount里写window.removeEventListener('scroll', this.handleScroll)的话,会发生什么?当你把悬浮窗组件销毁之后,handleScroll这个函数因为被window的事件监听器引用着,永远不会被垃圾回收机制回收,而且每次window滚动都会触发这个已经不存在的组件里的函数,轻则浪费浏览器的性能,重则因为组件已经不存在了,handleScroll里访问的this.$refs.xxx或者this.data.xxx变成undefined,直接报错。

除了window的全局事件,还有非绑定在组件根元素上的、动态创建的DOM元素的事件监听器,也需要手动清理,比如你在mounted里动态创建了一个canvas元素,然后给它绑定了mousemove、mousedown、mouseup三个事件来做画图功能,那在beforeUnmount里不仅要把canvas元素从DOM里移走,还要把这三个事件监听器都解绑,再把canvas元素的引用设为null,让垃圾回收能顺利收走它。

清理定时器、延时器、Web Worker、WebSocket这些异步资源

定时器和延时器也是内存泄漏和bug的重灾区,比如你做了一个倒计时组件,在mounted里用setInterval每秒更新一次倒计时数字,那如果不在beforeUnmount里clearInterval的话,组件销毁之后倒计时还在后台跑,不仅浪费性能,还可能导致组件已经不在了,但全局变量里的倒计时数字还在变(如果你的倒计时是用全局状态管理的还好,但如果是组件内部的data,虽然组件实例被引用着,但DOM已经不在了,变了也没用)。

Web Worker和WebSocket就更不用说了,这些都是浏览器分配的独立线程或者网络连接,Vue框架根本管不着,如果不在beforeUnmount里关闭Worker(worker.terminate())或者关闭WebSocket连接(socket.close()),不仅会占用浏览器的线程资源和服务器的网络连接资源,还可能在组件销毁之后收到Worker或者服务器发来的消息,导致代码报错。

清理第三方库的实例,比如ECharts、Three.js、Swiper这些

很多第三方库在初始化的时候会创建自己的内部实例、DOM元素、事件监听器甚至定时器,如果不在组件卸载前调用它们提供的销毁方法,这些资源都会残留下来,造成内存泄漏或者页面卡顿,比如ECharts,官方文档明确要求在组件卸载前调用myChart.dispose()来销毁实例,释放内存;Three.js需要调用renderer.dispose()、scene.dispose()、geometry.dispose()、material.dispose()来分别销毁渲染器、场景、几何体、材质;Swiper需要调用mySwiper.destroy(true, true)(第一个true表示删除Swiper添加的DOM元素,第二个true表示删除Swiper添加的事件监听器)。

这里有个小提示:很多第三方库的销毁方法都是异步的,或者需要等待某个动画结束才能调用,这时候怎么办?其实不用担心,beforeUnmount只是触发收尾工作的时机,并不是说必须在这个钩子的同步代码里完成所有清理,你可以在beforeUnmount里先停止一些动画或者阻止一些新的请求,然后等清理完成之后再释放引用就行,只要引用都释放了,垃圾回收机制早晚会收走的。

Vue3 Options API和Composition API里分别怎么正确使用beforeUnmount(对应beforeDestroy)?附具体代码示例

讲完了存在状态和使用场景,接下来就是最实用的部分:具体怎么写代码。

Vue3 Options API的写法(兼容老项目,也可以用,但推荐用新名beforeUnmount)

<template>
  <div class="floating-window">
    <p>这是一个监听window滚动的悬浮窗</p>
    <p>倒计时:{{ count }}</p>
    <div ref="echartsContainer" style="width: 400px; height: 300px;"></div>
  </div>
</template>
<script>
import * as echarts from 'echarts';
export default {
  name: 'FloatingWindow',
  data() {
    return {
      count: 10,
      timer: null,
      myChart: null,
      handleScroll: null // 这里把handleScroll存到data里,方便后面解绑
    };
  },
  mounted() {
    // 1. 绑定window scroll事件,注意这里要把函数存到data里,不然removeEventListener找不到同一个函数
    this.handleScroll = () => {
      console.log('window滚动了');
    };
    window.addEventListener('scroll', this.handleScroll);
    // 2. 启动定时器
    this.timer = setInterval(() => {
      this.count--;
      if (this.count === 0) {
        clearInterval(this.timer);
        this.timer = null;
      }
    }, 1000);
    // 3. 初始化ECharts
    this.myChart = echarts.init(this.$refs.echartsContainer);
    const option = {
      title: { text: 'ECharts示例' },
      xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
      yAxis: { type: 'value' },
      series: [{ data: [150, 230, 224, 218, 135, 147, 260], type: 'line' }]
    };
    this.myChart.setOption(option);
  },
  // 旧名写法(向后兼容,不推荐)
  // beforeDestroy() {
  //   this.cleanup();
  // },
  // 新名写法(官方推荐,语义更清晰)
  beforeUnmount() {
    this.cleanup();
  },
  methods: {
    // 把所有清理逻辑封装到一个cleanup方法里,代码更整洁
    cleanup() {
      // 1. 解绑window scroll事件
      if (this.handleScroll) {
        window.removeEventListener('scroll', this.handleScroll);
        this.handleScroll = null; // 释放引用
      }
      // 2. 清理定时器
      if (this.timer) {
        clearInterval(this.timer);
        this.timer = null;
      }
      // 3. 销毁ECharts实例
      if (this.myChart) {
        this.myChart.dispose();
        this.myChart = null;
      }
      console.log('悬浮窗组件即将卸载,所有清理工作已完成');
    }
  }
};
</script>
<style scoped>
.floating-window {
  position: fixed;
  top: 20px;
  right: 20px;
  width: 450px;
  padding: 20px;
  background-color: white;
  border: 1px solid #eee;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>

这里有两个关键点需要强调:第一,绑定原生DOM事件监听器的时候,一定要把函数存到一个可以访问的变量里(比如data或者闭包变量),不要直接用匿名函数,因为匿名函数每次调用都是一个新的函数,removeEventListener的时候找不到之前绑定的那个,自然就解绑不了;第二,清理完所有资源之后,最好把它们的引用设为null,这样可以更快地让垃圾回收机制识别到这些资源已经不再被引用,从而更快地释放内存。

然后是Vue3 Composition API的写法(官方推荐的新写法,更灵活,逻辑更集中)

Composition API里没有beforeDestroy,只有onBeforeUnmount这个组合式函数,而且它的使用非常方便,逻辑可以和初始化逻辑放在一起,不用像Options API那样分散在mounted、beforeUnmount、methods里。

<template>
  <div class="floating-window">
    <p>这是一个监听window滚动的悬浮窗</p>
    <p>倒计时:{{ count }}</p>
    <div ref="echartsContainer" style="width: 400px; height: 300px;"></div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts';
// 响应式数据
const count = ref(10);
const echartsContainer = ref(null);
// 变量存储异步资源和事件处理函数
let timer = null;
let myChart = null;
// 这里用const定义也行,因为函数本身不会变,只是闭包变量的问题?不对,const定义的函数是可以作为removeEventListener的参数的,因为函数是引用类型,只要引用不变就行
const handleScroll = () => {
  console.log('window滚动了');
};
// 初始化逻辑
onMounted(() => {
  // 1. 绑定window scroll事件
  window.addEventListener('scroll', handleScroll);
  // 2. 启动定时器
  timer = setInterval(() => {
    count.value--;
    if (count.value === 0) {
      clearInterval(timer);
      timer = null;
    }
  }, 1000);
  // 3. 初始化ECharts,注意这里要等DOM挂载完成,echartsContainer.value才有值,所以放在onMounted里是对的
  if (echartsContainer.value) {
    myChart = echarts.init(echartsContainer.value);
    const option = {
      title: { text: 'ECharts示例' },
      xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
      yAxis: { type: 'value' },
      series: [{ data: [150, 230, 224, 218, 135, 147, 260], type: 'line' }]
    };
    myChart.setOption(option);
  }
});
// 清理逻辑,和初始化逻辑放在一起,方便维护
onBeforeUnmount(() => {
  // 1. 解绑window scroll事件
  window.removeEventListener('scroll', handleScroll);
  // 2. 清理定时器
  if (timer) {
    clearInterval(timer);
    timer = null;
  }
  // 3. 销毁ECharts实例
  if (myChart) {
    myChart.dispose();
    myChart = null;
  }
  console.log('悬浮窗组件即将卸载,所有清理工作已完成');
});
</script>
<style scoped>
.floating-window {
  position: fixed;
  top: 20px;
  right: 20px;
  width: 450px;
  padding: 20px;
  background-color: white;
  border: 1px solid #eee;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>

Composition API的写法是不是更清晰?所有和悬浮窗相关的逻辑(滚动监听、倒计时、ECharts初始化和清理)都集中在setup函数里,不用像Options API那样分散在多个选项里,而且不需要把函数存到data里,直接用const定义在setup的顶层作用域里就行,因为setup函数只会执行一次,顶层作用域里的变量都是闭包变量,引用不会变,所以removeEventListener可以直接用。

有没有什么常见的错误用法?这三个坑千万别踩

讲完了正确用法,再讲三个平时开发中常见的错误用法,避免大家踩坑:

错误用法一:在beforeUnmount里做新的DOM操作或者异步请求

beforeUnmount的触发时机是组件从DOM树中被完全移除之前,这时候虽然组件的DOM元素还在,但马上就会被移除,所以做新的DOM操作(比如添加新元素、修改样式)是没有意义的,因为很快就会被删掉;做异步请求就更不对了,不仅可能导致组件销毁之后请求还在后台跑,浪费服务器资源,还可能在请求成功之后尝试修改已经不存在的组件的响应式数据,直接报错。

错误用法二:忘记清理手动绑定的事件监听器或者异步资源

这个我在前面已经讲过很多次了,是最常见的错误,也是内存泄漏的主要原因,大家一定要记住:只要是Vue框架不会自动帮你清理的东西,都要在beforeUnmount里手动清理。

错误用法三:在keep-alive包裹的组件里用beforeUnmount代替deactivated

很多新手会混淆keep-alive组件的生命周期钩子和普通组件的生命周期钩子,keep-alive包裹的组件,在第一次挂载的时候会触发mounted,然后每次切换到其他组件的时候会触发deactivated(组件被“停用”,放到keep-alive的缓存里,DOM元素还在缓存里,没有被移除),每次切换回来的时候会触发activated(组件被“激活”,从缓存里拿出来放到DOM树里),只有当组件被彻底从keep-alive的缓存里移除的时候(比如通过include/exclude数组把组件删掉,或者keep-alive的max属性满了,组件被挤出缓存),才会触发beforeUnmount和unmounted。

所以如果你的组件是被keep-alive包裹的,想要在组件被“停用”的时候清理一些临时资源(比如暂停视频、暂停动画),那应该用deactivated,而不是beforeUnmount;想要在组件被“激活”的时候恢复这些资源,应该用activated,而不是mounted(mounted只会在第一次挂载的时候触发一次)。

Vue3里beforeDestroy的核心知识点

我们再来总结一下这篇文章的核心知识点,方便大家快速回顾:

  1. 存在状态:Vue3 Options API保留了beforeDestroy,但它是beforeUnmount的向后兼容别名,新代码强烈推荐用beforeUnmount;Composition API没有beforeDestroy,只有onBeforeUnmount。
  2. 触发时机:组件从DOM树中被完全移除之前,这时候组件的响应式数据、DOM元素都还能正常访问。
  3. 核心作用:做组件卸载前的收尾工作,清理Vue框架不会自动清理的资源,防止内存泄漏或其他bug。
  4. 高频使用场景:清理手动绑定的全局/非根原生DOM事件监听器、清理定时器/Web Worker/WebSocket等异步资源、清理第三方库的实例。
  5. 常见错误用法:在beforeUnmount里做新的DOM操作或异步请求、忘记清理手动绑定的资源、在keep-alive组件里用beforeUnmount代替deactivated。

希望这篇文章能帮大家彻底搞懂Vue3里beforeDestroy的相关知识,以后再也不会在生命周期钩子的使用上踩坑啦!

版权声明

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

热门