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

先搞懂版本差异,Vue2 和 Vue3 销毁阶段钩子咋对应?

terry 5小时前 阅读数 7 #Vue

p>刚学Vue的同学经常搞混版本,比如把Vue3的beforeUnmount当成Vue2也有的钩子,其实Vue2里组件销毁前的生命周期叫beforeDestroy,Vue3才把名字改成beforeUnmount(选项式API),但不管名字咋变,“组件销毁前清理资源”的逻辑是共通的,今天就从Vue2的beforeDestroy说起,把“组件销毁前要干啥、咋干、踩啥坑”讲清楚,顺道理清版本差异,以后写代码不迷路。

得先把版本这层窗户纸捅破,不然写代码容易报错,Vue2 的选项式 API 里,组件销毁分两个阶段:beforeDestroy(销毁前)和 destroyed(销毁后),到了 Vue3,选项式 API 把这俩钩子改了名,变成 beforeUnmount(对应 beforeDestroy)和 unmounted(对应 destroyed);要是用组合式 API(setup 语法),则是 onBeforeUnmount、onUnmounted。

简单说,功能上 Vue2 的 beforeDestroy 和 Vue3 的 beforeUnmount 干的是同一件事——组件销毁(卸载)前,给你最后一次机会清理资源,只是 Vue3 为了更贴合「组件从 DOM 树卸载」的概念,把名字改成了 unmount 系列,所以别再把版本弄混啦~

Vue2 的 beforeDestroy(对应 Vue3 beforeUnmount)啥时候触发?

知道了名字对应关系,再看触发时机,当组件实例要被「销毁」时,beforeDestroy 会被调用,啥情况算「销毁」?比如路由切换时组件被替换、用 v-if 把组件从 DOM 里移除、手动调用 this.$destroy() 方法……

触发 beforeDestroy 时,组件处于啥状态?有三个关键特点:
1. 组件实例还在,data、methods 这些还能正常访问;
2. 组件对应的 DOM 还在页面上,没被移除;
3. 接下来就要执行「销毁逻辑」:解绑事件、清除响应式依赖、最终移除 DOM。

举个直观例子:你做了个弹窗组件,点关闭按钮时用 v-if="false" 把弹窗干掉,在弹窗消失前,beforeDestroy 会触发,这时候你可以在这个钩子裡关闭弹窗的动画、清理弹窗里的定时器。

beforeDestroy 主要用来解决哪些开发痛点?

要是组件里用了「外部资源」(比如定时器、事件监听、网络请求、第三方库),不清理就会出大问题,beforeDestroy 就是专门用来「擦屁股」的——把这些外部资源全清掉,避免内存泄漏、逻辑冲突,下面分场景唠:

场景1:清除定时器/间隔器

最常见的坑:mounted 里设了定时器,组件销毁后定时器还在跑,疯狂占内存,比如做倒计时组件,mounted 里用 setInterval 每秒减 1,要是不在 beforeDestroy 里 clearInterval,就算组件被销毁,定时器还会一直执行,页面越用越卡。

代码示例:

export default {
  data() { return { timer: null, count: 60 } },
  mounted() {
    this.timer = setInterval(() => {
      this.count--;
      if (this.count <= 0) clearInterval(this.timer);
    }, 1000);
  },
  beforeDestroy() {
    clearInterval(this.timer); // 销毁前清掉定时器
    this.timer = null; // 手动置空,帮垃圾回收器回收
  }
}

场景2:移除事件监听

比如给 window 加了 resize 事件,组件销毁后没移除,下次其他组件再绑 resize,就会触发多次回调,逻辑乱套。

代码示例(错误 vs 正确):
// 错误:只绑没卸

mounted() {
  window.addEventListener('resize', this.handleResize);
},
methods: {
  handleResize() { /* 调整组件尺寸 */ }
}  
// 正确:beforeDestroy 里卸
mounted() {
  window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
  window.removeEventListener('resize', this.handleResize);
},
methods: {
  handleResize() { /* 调整组件尺寸 */ }
}  

场景3:清理第三方库的副作用

比如用 Element UI 的 Dialog 组件,或者自己封装的弹层组件,组件销毁前得手动关闭弹层,否则浮层会残留页面,再比如用 Chart.js 画图表,组件销毁前要销毁图表实例,不然 canvas 元素占着内存不释放。

伪代码示例:

mounted() {
  this.chart = new Chart(this.$refs.canvas, { /* 配置 */ });
},
beforeDestroy() {
  this.chart.destroy(); // 销毁图表实例,释放资源
}  

场景4:中止未完成的网络请求

组件销毁后,要是之前发的请求还没回来,等请求成功后执行 this.$emit 或者修改 data,就会报错(因为组件实例已经被销毁,this 指向可能失效),这时候得用请求取消机制,axios 的 CancelToken。

代码示例:

mounted() {
  this.source = axios.CancelToken.source(); // 创建取消令牌
  axios.get('/api/getData', { 
    cancelToken: this.source.token 
  }).then(res => {
    this.data = res.data;
  }).catch(err => {
    if (axios.isCancel(err)) {
      console.log('组件销毁,请求已取消');
    }
  });
},
beforeDestroy() {
  this.source.cancel('组件销毁,取消请求'); // 销毁前取消请求
}  

这些场景总结成一句话:只要组件用了「不属于 Vue 响应式系统」的外部资源,beforeDestroy 就得负责清理,不然轻则内存泄漏,重则页面逻辑爆炸。

不用 beforeDestroy 清理资源,会有啥后果?

后果可不少,挑最常见的三个说:

内存泄漏

定时器、事件监听、第三方库实例这些东西,要是不主动清理,就算组件销毁了,它们还会占用内存,浏览器内存越用越多,页面越来越卡,甚至崩溃,比如一个单页应用(SPA),用户来回切换路由,每个页面的定时器都没清,用不了多久内存就爆了。

逻辑冲突

比如给 window 绑了 resize 事件,组件销毁后没解绑,下次进入相同组件又绑一次,resize 时同一个回调会执行多次,原本想调整一次布局,结果被执行 N 次,页面样式直接乱套。

报错崩溃

最直观的是网络请求:组件销毁后,请求才返回,这时候执行 this.$emit('xxx') 或者 this.data = res.data,就会因为「组件实例已销毁,this 指向错误」报错,用户能看到满屏的红色错误,体验直接拉胯。

所以别嫌麻烦,该清理的资源一定要在 beforeDestroy 里清干净~

和 mounted、updated 这些钩子配合,咋设计组件生命周期逻辑?

组件的生命周期钩子是一套「初始化→更新→销毁」的流程,每个钩子各司其职,举个「实时刷新表格」的例子,把 mounted、updated、beforeDestroy 串起来看:

export default {
  data() {
    return {
      tableData: [],
      timer: null,
      isLoading: false
    };
  },
  // 初始化阶段:mounted 干「绑定资源+初始化请求」
  mounted() {
    this.fetchData(); // 第一次请求数据,渲染表格
    this.timer = setInterval(() => {
      this.fetchData(); // 每5秒自动刷新
    }, 5000);
    window.addEventListener('resize', this.handleResize); // 绑窗口变化事件
  },
  methods: {
    async fetchData() {
      this.isLoading = true;
      try {
        const res = await axios.get('/api/tableData');
        this.tableData = res.data;
      } catch (err) {
        console.error('请求失败:', err);
      } finally {
        this.isLoading = false;
      }
    },
    handleResize() {
      // 窗口变化时,调整表格列宽(假设用了第三方表格库)
      this.$refs.table.resize();
    }
  },
  // 更新阶段:updated 干「DOM更新后调整样式」
  updated() {
    this.$nextTick(() => {
      this.adjustScroll(); // 表格数据更新后,调整滚动条位置
    });
  },
  // 销毁阶段:beforeDestroy 干「清理资源」
  beforeDestroy() {
    clearInterval(this.timer); // 清定时器
    window.removeEventListener('resize', this.handleResize); // 卸事件监听
    if (this.isLoading) {
      // 如果请求还没完成,这里可以加逻辑取消请求(参考之前的axios例子)
    }
  },
  methods: {
    adjustScroll() {
      // 假设表格有滚动条,数据更新后定位到顶部
      this.$refs.table.scrollTop = 0;
    }
  }
}

从这个例子能看出分工:
- mounted:负责「初始化」,把需要的资源(定时器、事件监听、网络请求)都启动/绑定;
- updated:负责「更新后」,DOM 重新渲染完,做一些样式、滚动位置之类的调整;
- beforeDestroy:负责「销毁前」,把 mounted 阶段绑的资源全清掉,避免后遗症。

这种分工能让组件的生命周期逻辑清晰,哪里出问题也好排查。

实际开发中,beforeDestroy 容易踩哪些坑?咋避坑?

就算知道要清理资源,实操时还是容易掉坑里,总结四个高频坑和解决办法:

坑1:忘记清理事件监听/定时器

现象:mounted 里绑了事件或定时器,beforeDestroy 里忘写清理代码。
解决:养成「绑定和清理成对写」的习惯,比如写 mounted 时,先想清楚哪些资源需要在 beforeDestroy 清理,直接把清理代码的框架写上,再补逻辑。

坑2:this 指向错误

现象:在定时器、事件监听的回调里,this 不是组件实例,导致清理失败,比如用普通函数写回调,this 指向 window。
解决:回调用箭头函数,或者用 bind(this) 固定 this 指向。

反面例子(错误):

mounted() {
  this.timer = setInterval(function() {
    this.fetchData(); // this 是 window,不是组件!
  }, 1000);
}  

正确写法(二选一):

// 方法1:箭头函数
this.timer = setInterval(() => {
  this.fetchData();
}, 1000);  
<p>// 方法2:bind(this)
this.timer = setInterval(function() {
this.fetchData();
}.bind(this), 1000);<br />

坑3:异步操作在销毁后执行

现象:网络请求、定时器回调在组件销毁后才执行,导致操作已销毁的组件报错。
解决:在 beforeDestroy 里主动取消异步操作,axios 请求用 CancelToken,定时器用 clearInterval,Promise 用 AbortController(fetch API 的取消方式)。

坑4:子组件没正确销毁

现象:父组件用 v-if 控制子组件显示,子组件里的资源没清理,比如子组件有定时器,父组件销毁子组件时,子组件的 beforeDestroy 没触发?不,父组件销毁时,子组件也会触发自己的 beforeDestroy,但如果子组件是通过 keep-alive 缓存的,销毁逻辑要注意(keep-alive 用 activated、deactivated 钩子)。
解决:子组件自己的 beforeDestroy 里处理自己的资源,别依赖父组件,父组件只管自己的清理,子组件对自己的资源负责。

避坑口诀:绑定必清理,this 要盯紧,异步要中止,子组件自扫门前雪

Vue3 里的 beforeUnmount 和 Vue2 beforeDestroy 有啥不一样?

功能上几乎没区别,就是命名和 API 风格的变化:

- Vue2 选项式 API 用 beforeDestroy,Vue3 选项式 API 改成 beforeUnmount(对应 destroyed → unmounted);
- Vue3 组合式 API(setup 里)用 onBeforeUnmount,写法更函数式。

举个 Vue3 选项式 API 的例子,和 Vue2 逻辑完全一致:

// Vue3 选项式 API
export default {
  data() { return { timer: null } },
  mounted() {
    this.timer = setInterval(() => { ... }, 1000);
  },
  beforeUnmount() { // 这里改名成 beforeUnmount 了
    clearInterval(this.timer);
  }
}
<p>// Vue3 组合式 API
import { onMounted, onBeforeUnmount } from 'vue';
export default {
setup() {
let timer = null;
onMounted(() => {
timer = setInterval(() => { ... }, 1000);
});
onBeforeUnmount(() => { // 组合式 API 的钩子
clearInterval(timer);
});
return {};
}
}

所以核心逻辑没变:组件卸载前清理资源,不管 Vue2 还是 Vue3,只要涉及「外部资源」,这个环节不能少。

有没有必要在每个组件里都写 beforeDestroy(beforeUnmount)?

分情况:

- 需要写的情况:组件用了「外部资源」(定时器、事件监听、网络请求、第三方库实例),必须写,比如轮播图组件(有定时器)、实时刷新组件(有定时器+请求)、带窗口监听的布局组件。
- 不用写的情况:纯展示型组件,只渲染 data 里的数据,没有任何外部绑定,比如一个纯展示的卡片组件,只负责把 props 渲染成 DOM,没有定时器、事件监听,就不用写。

举个🌰:一个「用户信息卡片」组件,props 接收用户姓名、头像,模板里渲染出来,没有任何交互逻辑,这种组件就不需要 beforeDestroy,因为没有外部资源要清理。

所以别盲目写,先看组件有没有「外部依赖」,有就清,没有就省点代码~

手动调用 $destroy 时,beforeDestroy 会触发吗?

会触发!Vue 实例的 $destroy 方法,就是用来手动触发销毁流程的,调用 this.$destroy() 后,Vue 会依次执行 beforeDestroy 和 destroyed 钩子。

但要注意:手动调用 $destroy 后,组件实例会被标记为「已销毁」,但 DOM 不会自动移除(除非组件是被 v-if 等指令控制的,v-if 会自动处理 DOM 移除),所以如果是手动销毁,还得自己处理 DOM 移除的话,要额外写代码。

举个例子:

export default {
  methods: {
    handleClose() {
      this.$destroy(); // 触发 beforeDestroy 和 destroyed
      this.$el.parentNode.removeChild

版权声明

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

发表评论:

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

热门