生命周期应用场景

生命周期钩子是 Vue 开发中不可或缺的工具。本章汇总各种实际应用场景,帮助你选择正确的钩子函数。

场景分类速查

场景推荐钩子说明
发起异步请求created尽早获取数据
操作 DOMmountedDOM 已渲染
初始化第三方库mounted需要 DOM 存在
添加事件监听mounted需要访问元素
清除定时器beforeDestroy实例仍可用
移除事件监听beforeDestroy防止内存泄漏
销毁第三方库beforeDestroy释放资源
监听数据变化watch更精确控制
DOM 更新后操作updated + $nextTick确保 DOM 已更新

数据请求场景

基础数据请求

export default {
  data() {
    return {
      user: null,
      loading: true,
      error: null
    }
  },
  created() {
    this.fetchUser()
  },
  methods: {
    async fetchUser() {
      try {
        const response = await api.getUser()
        this.user = response.data
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    }
  }
}

依赖参数的数据请求

当数据请求依赖 props 时,使用 watch:

export default {
  props: {
    userId: {
      type: Number,
      required: true
    }
  },
  data() {
    return {
      user: null
    }
  },
  created() {
    this.fetchUser()
  },
  watch: {
    userId() {
      this.fetchUser()
    }
  },
  methods: {
    async fetchUser() {
      this.user = await api.getUser(this.userId)
    }
  }
}

并行请求

export default {
  data() {
    return {
      user: null,
      posts: null,
      ready: false
    }
  },
  async created() {
    const [user, posts] = await Promise.all([
      api.getUser(),
      api.getPosts()
    ])
    
    this.user = user
    this.posts = posts
    this.ready = true
  }
}

轮询数据

export default {
  data() {
    return {
      data: null,
      timer: null
    }
  },
  mounted() {
    this.fetchData()
    this.startPolling()
  },
  beforeDestroy() {
    this.stopPolling()
  },
  methods: {
    async fetchData() {
      this.data = await api.getData()
    },
    startPolling() {
      this.timer = setInterval(this.fetchData, 30000)
    },
    stopPolling() {
      clearInterval(this.timer)
    }
  }
}

DOM 操作场景

获取元素尺寸

export default {
  data() {
    return {
      width: 0,
      height: 0
    }
  },
  mounted() {
    this.measureElement()
    window.addEventListener('resize', this.measureElement)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.measureElement)
  },
  methods: {
    measureElement() {
      const rect = this.$refs.container.getBoundingClientRect()
      this.width = rect.width
      this.height = rect.height
    }
  }
}

自动聚焦输入框

export default {
  template: `
    <input ref="input" v-model="value">
  `,
  props: {
    autofocus: {
      type: Boolean,
      default: false
    }
  },
  mounted() {
    if (this.autofocus) {
      this.$refs.input.focus()
    }
  }
}

滚动到指定位置

export default {
  props: {
    scrollToBottom: Boolean
  },
  mounted() {
    if (this.scrollToBottom) {
      this.$nextTick(() => {
        const container = this.$refs.container
        container.scrollTop = container.scrollHeight
      })
    }
  },
  updated() {
    if (this.scrollToBottom) {
      this.$nextTick(() => {
        const container = this.$refs.container
        container.scrollTop = container.scrollHeight
      })
    }
  }
}

第三方库集成场景

ECharts 集成

export default {
  template: '<div ref="chart" style="width: 100%; height: 400px;"></div>',
  props: {
    option: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      chart: null
    }
  },
  mounted() {
    this.initChart()
    window.addEventListener('resize', this.handleResize)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize)
    if (this.chart) {
      this.chart.dispose()
    }
  },
  watch: {
    option: {
      deep: true,
      handler(newOption) {
        if (this.chart) {
          this.chart.setOption(newOption)
        }
      }
    }
  },
  methods: {
    initChart() {
      this.chart = echarts.init(this.$refs.chart)
      this.chart.setOption(this.option)
    },
    handleResize() {
      this.chart?.resize()
    }
  }
}

拖拽库集成

export default {
  template: `
    <ul ref="list">
      <li v-for="item in items" :key="item.id" :data-id="item.id">
        {{ item.name }}
      </li>
    </ul>
  `,
  props: {
    items: Array
  },
  data() {
    return {
      sortable: null
    }
  },
  mounted() {
    this.initSortable()
  },
  beforeDestroy() {
    if (this.sortable) {
      this.sortable.destroy()
    }
  },
  methods: {
    initSortable() {
      this.sortable = new Sortable(this.$refs.list, {
        animation: 150,
        onEnd: (evt) => {
          const { oldIndex, newIndex } = evt
          this.$emit('sort', { oldIndex, newIndex })
        }
      })
    }
  }
}

事件监听场景

全局键盘事件

export default {
  data() {
    return {
      showSearch: false
    }
  },
  mounted() {
    document.addEventListener('keydown', this.handleKeydown)
  },
  beforeDestroy() {
    document.removeEventListener('keydown', this.handleKeydown)
  },
  methods: {
    handleKeydown(e) {
      if (e.key === '/' && !this.showSearch) {
        e.preventDefault()
        this.showSearch = true
      }
      if (e.key === 'Escape' && this.showSearch) {
        this.showSearch = false
      }
    }
  }
}

窗口大小变化

export default {
  data() {
    return {
      windowWidth: 0,
      windowHeight: 0,
      isMobile: false
    }
  },
  mounted() {
    this.updateWindowSize()
    window.addEventListener('resize', this.updateWindowSize)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.updateWindowSize)
  },
  methods: {
    updateWindowSize() {
      this.windowWidth = window.innerWidth
      this.windowHeight = window.innerHeight
      this.isMobile = this.windowWidth < 768
    }
  }
}

点击外部关闭

export default {
  template: `
    <div class="dropdown" v-if="isOpen">
      <slot></slot>
    </div>
  `,
  data() {
    return {
      isOpen: false
    }
  },
  mounted() {
    document.addEventListener('click', this.handleClickOutside)
  },
  beforeDestroy() {
    document.removeEventListener('click', this.handleClickOutside)
  },
  methods: {
    toggle() {
      this.isOpen = !this.isOpen
    },
    handleClickOutside(e) {
      if (!this.$el.contains(e.target)) {
        this.isOpen = false
      }
    }
  }
}

资源清理场景

定时器清理

export default {
  data() {
    return {
      countdown: 60,
      timer: null
    }
  },
  mounted() {
    this.startCountdown()
  },
  beforeDestroy() {
    this.stopCountdown()
  },
  methods: {
    startCountdown() {
      this.timer = setInterval(() => {
        if (this.countdown > 0) {
          this.countdown--
        } else {
          this.stopCountdown()
          this.$emit('finish')
        }
      }, 1000)
    },
    stopCountdown() {
      if (this.timer) {
        clearInterval(this.timer)
        this.timer = null
      }
    }
  }
}

WebSocket 清理

export default {
  data() {
    return {
      socket: null,
      messages: []
    }
  },
  mounted() {
    this.connect()
  },
  beforeDestroy() {
    this.disconnect()
  },
  methods: {
    connect() {
      this.socket = new WebSocket(this.url)
      
      this.socket.onopen = () => {
        console.log('Connected')
      }
      
      this.socket.onmessage = (event) => {
        this.messages.push(JSON.parse(event.data))
      }
      
      this.socket.onerror = (error) => {
        console.error('WebSocket error:', error)
      }
    },
    disconnect() {
      if (this.socket) {
        this.socket.close()
        this.socket = null
      }
    }
  }
}

取消异步请求

export default {
  data() {
    return {
      abortController: null,
      data: null
    }
  },
  methods: {
    async fetchData() {
      this.abortController = new AbortController()
      
      try {
        const response = await fetch('/api/data', {
          signal: this.abortController.signal
        })
        this.data = await response.json()
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Fetch error:', error)
        }
      }
    },
    cancelRequest() {
      if (this.abortController) {
        this.abortController.abort()
      }
    }
  },
  beforeDestroy() {
    this.cancelRequest()
  }
}

keep-alive 场景

缓存组件状态

export default {
  template: `
    <keep-alive>
      <component :is="currentTab"></component>
    </keep-alive>
  `,
  data() {
    return {
      currentTab: 'home'
    }
  }
}

Vue.component('home', {
  template: '<div>Home: {{ count }}</div>',
  data() {
    return {
      count: 0
    }
  },
  activated() {
    console.log('Home activated')
  },
  deactivated() {
    console.log('Home deactivated')
  }
})

恢复滚动位置

export default {
  data() {
    return {
      scrollTop: 0
    }
  },
  activated() {
    this.$nextTick(() => {
      this.$refs.container.scrollTop = this.scrollTop
    })
  },
  deactivated() {
    this.scrollTop = this.$refs.container.scrollTop
  }
}

错误处理场景

全局错误捕获

new Vue({
  el: '#app',
  errorCaptured(err, vm, info) {
    console.error('Error:', err)
    console.error('Component:', vm)
    console.error('Info:', info)
    
    this.$notify({
      type: 'error',
      message: '发生错误,请稍后重试'
    })
    
    return false
  }
})

组件级错误处理

export default {
  data() {
    return {
      error: null
    }
  },
  errorCaptured(err, vm, info) {
    this.error = err.message
    return false
  }
}

最佳实践总结

钩子选择原则

  1. created:数据初始化、异步请求
  2. mounted:DOM 操作、第三方库初始化
  3. updated:DOM 更新后的操作(谨慎使用)
  4. beforeDestroy:资源清理

避免常见错误

1. 不要在 updated 中直接修改数据

updated() {
  this.counter++
}

2. 不要忘记清理资源

mounted() {
  this.timer = setInterval(this.poll, 5000)
}

3. 使用 $nextTick 确保 DOM 更新

this.items.push(item)
this.$nextTick(() => {
  this.scrollToBottom()
})

性能优化建议

  1. 使用 computed 替代 updated 中的计算
  2. 使用 watch 进行精确控制
  3. 合理使用 v-once 和 Object.freeze
  4. 避免在 created/mounted 中进行大量计算