销毁阶段

销毁阶段是 Vue 实例生命周期的最后一个阶段。在这个阶段,Vue 会清理实例与 DOM 的绑定、移除事件监听器、销毁子组件等。

销毁阶段包含两个钩子:beforeDestroy 和 destroyed。

beforeDestroy 钩子

beforeDestroy 在实例销毁之前调用。此时实例仍然完全可用。

执行时机

vm.$destroy() 调用
       │
       ▼
beforeDestroy  ← 实例仍然可用
       │
       ▼
移除 watchers
移除事件监听器
销毁子组件
       │
       ▼
destroyed

特点

  • 实例仍然可用
  • 可以访问 data、methods
  • 可以访问 this.$el
  • 适合清理工作

示例

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 钩子

destroyed 在实例销毁后调用。此时实例已被清理,所有绑定已移除。

执行时机

beforeDestroy
       │
       ▼
移除 watchers
移除事件监听器
销毁子组件
       │
       ▼
destroyed  ← 实例已销毁

特点

  • 实例已销毁
  • 所有指令绑定已移除
  • 所有事件监听器已移除
  • 所有子组件已销毁
  • DOM 仍然存在(需要手动移除)

示例

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 与销毁

使用 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 与销毁

v-show 不会触发销毁,只是切换 display 属性:

<child-component v-show="show"></child-component>

使用 v-show 时,组件不会被销毁,只是隐藏显示。

keep-alive 与销毁

被 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
      }
    }
  }
})

WebSocket 连接组件

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>