一、vue3生命周期

如果把组件看作一个活生生的“生命体”,生命周期钩子就是这个生命体在出生、长大、生病、死亡各个阶段自动触发的“警报器”。理解它们,是控制组件行为、性能优化的核心。

1. Vue2 和 Vue3 生命周期对比

Vue3 对生命周期做了一次优雅的翻新。最大的变化是:去掉了 beforeCreatecreated,直接用 setup 代替;同时,所有的销毁钩子更名为“卸载”(Unmount),语义更加准确。

核心钩子对应表

阶段 Vue2 选项式 API Vue3 组合式 API 触发时机与核心用途
创建 beforeCreate created 直接写在 setup 顶层 组件实例刚创建。通常在这里发送初始的 Ajax 请求、初始化变量
挂载 beforeMount
mounted
onBeforeMount
onMounted
虚拟 DOM 挂载到真实页面上。onMounted 阶段才可以安全地操作 DOM
更新 beforeUpdate
updated
onBeforeUpdate
onUpdated
页面依赖的响应式数据发生改变,引发界面重新渲染
销毁 beforeDestroy
destroyed
onBeforeUnmount
onUnmounted
组件被切换或隐藏(如 v-if="false")。必须在这里清除定时器、解绑全局事件,否则会导致内存泄漏

2. Vue3 生命周期的标准写法

在 Vue3 中,生命周期钩子变成了按需引入的函数。你可以在同一个组件里多次调用同一个钩子,它们会按照编写顺序依次执行。

完整代码示范 (LifecycleDemo.vue)

<template>
  <div>
    <h3>当前计数:{{ count }}</h3>
    <button @click="count++">点击更新界面</button>
  </div>
</template>

<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

// 1. 替代了 created:直接在 setup 顶层写代码
console.log('1. 组件创建了!(相当于 created)')
const count = ref(0)

// 模拟异步获取数据
const loadData = () => { 
  console.log('Ajax 请求发送中...') 
}
loadData()

// 2. 挂载完成
onMounted(() => {
  console.log('2. 页面渲染完毕了!(mounted),现在可以获取 DOM 元素')
  // 比如在这里开启一个全局监听
  window.addEventListener('resize', onResize)
})

// 3. 数据更新
onUpdated(() => {
  console.log('3. 界面因为 count 改变而重新渲染了!(updated)')
})

// 4. 卸载/销毁
onUnmounted(() => {
  console.log('4. 组件死掉了!(unmounted)')
  // 极其重要:必须在这里清理垃圾,防止内存泄漏
  window.removeEventListener('resize', onResize)
})

const onResize = () => { 
  console.log('窗口大小变了') 
}
</script>

二、自定义 Hooks(组合式 API 的灵魂)

1. 什么是 Hooks?为什么要用它?

在 Vue2 中,如果我们想在多个组件之间复用一段公共逻辑(比如:监听鼠标坐标、倒计时功能),我们只能用 Mixin(混入)。但 Mixin 存在严重的致命缺点:数据来源不明确、容易发生命名冲突,项目放大后极其难维护。

Vue3 的自定义 Hooks 彻底解决了这个问题。

Hooks 的本质:利用 Vue3 的响应式 API(ref, reactive)和生命周期钩子,把一段完整的业务逻辑封装成一个独立的 .js.ts 函数。

2. 封装一个“实时获取鼠标坐标”的 Hook

第一步:创建 Hook 文件 (src/hooks/useMousePosition.js)

约定俗成,Hooks 文件和函数通常以 use 开头。

import { ref, onMounted, onUnmounted } from 'vue'

// 暴露一个公共函数
export function useMousePosition() {
  // 1. 在 Hook 内部定义响应式数据
  const x = ref(0)
  const y = ref(0)

  // 2. 复杂的业务逻辑
  const updatePosition = (e) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 3. 配合生命周期:挂载时监听,销毁时移除
  onMounted(() => {
    window.addEventListener('mousemove', updatePosition)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', updatePosition)
  })

  // 4. 核心:把数据和方法返回出去供组件使用
  return { x, y }
}

第二步:在任意组件中轻松引入和使用 (MyComponent.vue)
<template>
  <div class="box">
    <h4>当前鼠标坐标:X: {{ x }}, Y: {{ y }}</h4>
  </div>
</template>

<script setup>
// 引入我们的自定义 Hook
import { useMousePosition } from '../hooks/useMousePosition'

// 像解构普通对象一样拿出来用,变量来源一目了然,绝不冲突!
const { x, y } = useMousePosition()
</script>

3. 致命大坑:Hooks 里的响应式丢失问题

这是初学者写 Hooks 最容易踩的逻辑地雷:不要在 Hook 内部用 reactive 声明对象并直接解构返回!

错误示范:
// 在 useMousePosition.js 内部
import { reactive } from 'vue'

export function useMousePosition() {
  const pos = reactive({ x: 0, y: 0 })
  
  // ...监听逻辑
  
  // 错误返回:直接返回 pos,或者 return { ...pos }
  return pos 
}

  • 为什么错误?
    如果在组件里写 const { x, y } = useMousePosition(),在解构的那一瞬间,xy 就会退化成普通的数字 0,彻底失去响应式!鼠标再怎么动,界面也不会变。
两种标准正确写法:
  • 解法一(最推荐):像我们上面的正确示范一样,内部全部用独立的 ref 定义,最后包裹在一个普通对象里返回。
  • 解法二:内部用 reactive,但返回时使用 toRefs 函数包装一层:
import { reactive, toRefs } from 'vue'

// ...内部监听逻辑不变

return toRefs(pos) 
// toRefs 会把 reactive 内部的属性全部转为 ref,这样在组件中解构也不会丢失响应式!

更多推荐