销毁阶段是 Vue 实例生命周期的最后一个阶段。在这个阶段,Vue 会清理实例与 DOM 的绑定、移除事件监听器、销毁子组件等。
销毁阶段包含两个钩子:beforeDestroy 和 destroyed。
beforeDestroy 在实例销毁之前调用。此时实例仍然完全可用。
vm.$destroy() 调用
│
▼
beforeDestroy ← 实例仍然可用
│
▼
移除 watchers
移除事件监听器
销毁子组件
│
▼
destroyed
new Vue({
data: {
message: 'Hello'
},
beforeDestroy() {
console.log('beforeDestroy')
console.log('数据:', this.message)
console.log('DOM:', this.$el)
}
})
beforeDestroy 是执行清理工作的最佳时机:
1. 清除定时器
new Vue({
data() {
return {
timer: null
}
},
mounted() {
this.timer = setInterval(() => {
this.poll()
}, 5000)
},
beforeDestroy() {
clearInterval(this.timer)
}
})
2. 移除事件监听
new Vue({
mounted() {
window.addEventListener('resize', this.handleResize)
document.addEventListener('click', this.handleClick)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
document.removeEventListener('click', this.handleClick)
}
})
3. 取消异步请求
new Vue({
data() {
return {
requestController: null
}
},
methods: {
fetchData() {
this.requestController = new AbortController()
fetch('/api/data', { signal: this.requestController.signal })
.then(res => res.json())
.then(data => {
this.data = data
})
}
},
beforeDestroy() {
if (this.requestController) {
this.requestController.abort()
}
}
})
4. 清理第三方库实例
new Vue({
mounted() {
this.chart = new Chart(this.$refs.canvas, this.config)
this.editor = new CodeMirror(this.$refs.editor, this.options)
},
beforeDestroy() {
if (this.chart) {
this.chart.destroy()
}
if (this.editor) {
this.editor.toTextArea()
}
}
})
5. 移除全局事件总线监听
new Vue({
created() {
eventBus.$on('user-login', this.handleLogin)
eventBus.$on('user-logout', this.handleLogout)
},
beforeDestroy() {
eventBus.$off('user-login', this.handleLogin)
eventBus.$off('user-logout', this.handleLogout)
}
})
destroyed 在实例销毁后调用。此时实例已被清理,所有绑定已移除。
beforeDestroy
│
▼
移除 watchers
移除事件监听器
销毁子组件
│
▼
destroyed ← 实例已销毁
new Vue({
destroyed() {
console.log('实例已销毁')
console.log(this.$el)
}
})
destroyed 使用场景较少,偶尔用于:
1. 最终日志记录
destroyed() {
console.log('组件已销毁:', this.$options.name)
analytics.track('component_destroyed', {
component: this.$options.name
})
}
2. 通知其他组件
destroyed() {
eventBus.$emit('component-destroyed', this.id)
}
使用 $destroy 方法手动销毁实例:
const vm = new Vue({
template: '<div>{{ message }}</div>',
data: {
message: 'Hello'
}
}).$mount()
document.body.appendChild(vm.$el)
setTimeout(() => {
vm.$destroy()
document.body.removeChild(vm.$el)
}, 5000)
注意:$destroy 不会移除 DOM 元素,需要手动移除。
使用 v-if 切换组件会触发销毁:
<div id="app">
<button @click="show = !show">Toggle</button>
<child-component v-if="show"></child-component>
</div>
<script>
Vue.component('child-component', {
template: '<div>Child Component</div>',
beforeDestroy() {
console.log('子组件即将销毁')
},
destroyed() {
console.log('子组件已销毁')
}
})
new Vue({
el: '#app',
data: {
show: true
}
})
</script>
当 show 变为 false 时,子组件会被销毁。
v-show 不会触发销毁,只是切换 display 属性:
<child-component v-show="show"></child-component>
使用 v-show 时,组件不会被销毁,只是隐藏显示。
被 keep-alive 缓存的组件有特殊的生命周期:
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
首次创建: created → mounted → activated
切换离开: deactivated
再次激活: activated
最终销毁: deactivated → beforeDestroy → destroyed
Vue.component('cached-component', {
template: '<div>Cached Component</div>',
created() {
console.log('created')
},
mounted() {
console.log('mounted')
},
activated() {
console.log('activated')
},
deactivated() {
console.log('deactivated')
},
beforeDestroy() {
console.log('beforeDestroy')
},
destroyed() {
console.log('destroyed')
}
})
父组件销毁时,子组件也会被销毁:
父 beforeDestroy
子 beforeDestroy
子 destroyed
父 destroyed
Vue.component('child', {
template: '<div>Child</div>',
beforeDestroy() { console.log('子 beforeDestroy') },
destroyed() { console.log('子 destroyed') }
})
new Vue({
template: '<div><child></child></div>',
beforeDestroy() { console.log('父 beforeDestroy') },
destroyed() { console.log('父 destroyed') }
}).$mount()
vm.$destroy()
输出结果:
父 beforeDestroy
子 beforeDestroy
子 destroyed
父 destroyed
Vue.component('polling-data', {
template: `
<div>
<div v-if="loading">加载中...</div>
<div v-else>{{ data }}</div>
</div>
`,
props: {
url: String,
interval: {
type: Number,
default: 5000
}
},
data() {
return {
data: null,
loading: true,
timer: null
}
},
mounted() {
this.fetch()
this.startPolling()
},
beforeDestroy() {
this.stopPolling()
},
methods: {
async fetch() {
try {
const response = await fetch(this.url)
this.data = await response.json()
} catch (error) {
console.error('获取数据失败:', error)
} finally {
this.loading = false
}
},
startPolling() {
this.timer = setInterval(() => {
this.fetch()
}, this.interval)
},
stopPolling() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
}
})
Vue.component('websocket-client', {
template: '<div><slot :messages="messages"></slot></div>',
props: {
url: String
},
data() {
return {
socket: null,
messages: []
}
},
mounted() {
this.connect()
},
beforeDestroy() {
this.disconnect()
},
methods: {
connect() {
this.socket = new WebSocket(this.url)
this.socket.onopen = () => {
console.log('WebSocket 连接成功')
}
this.socket.onmessage = (event) => {
this.messages.push(JSON.parse(event.data))
}
this.socket.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
this.socket.onclose = () => {
console.log('WebSocket 连接关闭')
}
},
disconnect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
},
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data))
}
}
}
})
Vue.component('video-player', {
template: `
<div class="video-container">
<video ref="video" :src="src"></video>
<div class="controls">
<button @click="togglePlay">{{ playing ? '暂停' : '播放' }}</button>
<span>{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
</div>
</div>
`,
props: {
src: String
},
data() {
return {
playing: false,
currentTime: 0,
duration: 0
}
},
mounted() {
const video = this.$refs.video
video.addEventListener('play', this.onPlay)
video.addEventListener('pause', this.onPause)
video.addEventListener('timeupdate', this.onTimeUpdate)
video.addEventListener('loadedmetadata', this.onLoadedMetadata)
},
beforeDestroy() {
const video = this.$refs.video
video.removeEventListener('play', this.onPlay)
video.removeEventListener('pause', this.onPause)
video.removeEventListener('timeupdate', this.onTimeUpdate)
video.removeEventListener('loadedmetadata', this.onLoadedMetadata)
video.pause()
},
methods: {
togglePlay() {
const video = this.$refs.video
if (this.playing) {
video.pause()
} else {
video.play()
}
},
onPlay() {
this.playing = true
},
onPause() {
this.playing = false
},
onTimeUpdate() {
this.currentTime = this.$refs.video.currentTime
},
onLoadedMetadata() {
this.duration = this.$refs.video.duration
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
}
})
Vue.component('map-component', {
template: '<div ref="map" style="width: 100%; height: 400px;"></div>',
props: {
center: {
type: Object,
default: () => ({ lat: 39.9042, lng: 116.4074 })
},
zoom: {
type: Number,
default: 12
}
},
data() {
return {
map: null,
markers: []
}
},
mounted() {
this.initMap()
},
beforeDestroy() {
this.destroyMap()
},
methods: {
initMap() {
this.map = new google.maps.Map(this.$refs.map, {
center: this.center,
zoom: this.zoom
})
},
destroyMap() {
this.markers.forEach(marker => marker.setMap(null))
this.markers = []
this.map = null
},
addMarker(position, title) {
const marker = new google.maps.Marker({
position,
map: this.map,
title
})
this.markers.push(marker)
return marker
},
removeMarker(marker) {
const index = this.markers.indexOf(marker)
if (index > -1) {
marker.setMap(null)
this.markers.splice(index, 1)
}
}
}
})
不正确的销毁会导致内存泄漏:
1. 未清除定时器
mounted() {
this.timer = setInterval(() => {}, 1000)
}
2. 未移除事件监听
mounted() {
window.addEventListener('resize', this.handler)
}
3. 未取消异步请求
mounted() {
fetch('/api/data').then(data => {
this.data = data
})
}
4. 未清理第三方库
mounted() {
this.chart = new Chart(ctx, config)
}
{
data() {
return {
timer: null,
abortController: null,
chart: null
}
},
mounted() {
this.timer = setInterval(this.poll, 5000)
window.addEventListener('resize', this.handleResize)
this.abortController = new AbortController()
fetch('/api/data', { signal: this.abortController.signal })
.then(res => res.json())
.then(data => { this.data = data })
this.chart = new Chart(this.$refs.canvas, this.config)
},
beforeDestroy() {
clearInterval(this.timer)
window.removeEventListener('resize', this.handleResize)
this.abortController?.abort()
this.chart?.destroy()
}
}
1. 清理工作放在 beforeDestroy
beforeDestroy() {
this.cleanup()
}
2. 使用变量存储需要清理的资源
data() {
return {
timer: null,
listeners: []
}
}
3. 及时清理,避免累积
beforeDestroy() {
this.timer && clearInterval(this.timer)
this.socket && this.socket.close()
}
4. 考虑使用 keep-alive 缓存
<keep-alive>
<component :is="current"></component>
</keep-alive>