人人都会AI编程

11.4 Hooks 封装的常见坑点与注意事项

更新时间:2026-07-09

自定义 Hooks 让逻辑复用变得优雅,但也容易因为对响应式系统理解不够而写出“看起来对、实际跑偏”的代码。以下是在实际开发中最常踩的坑,以及对应的解决思路。

1. 解构丢失响应式

问题:直接从 reactive 对象解构出属性,或从 ref 解构出 value,拿到的是普通值,不再是响应式。

// 错误示例
function useUser() {
  const state = reactive({ name: 'Alice', age: 25 })
  return { name: state.name, age: state.age } // 解构后丢失响应式
}

解决:使用 toRefs 包装后再解构,或者直接用 ref 来定义独立状态。

// 正确示例
function useUser() {
  const state = reactive({ name: 'Alice', age: 25 })
  return { ...toRefs(state) } // 保留了响应式连接
}

2. 直接替换 reactive 对象

问题:用一个新对象整体替换 reactive 的值,会导致原响应式代理断开。

const state = reactive({ list: [] })
state.list = [1, 2, 3] // 这是可以的,修改属性
state = reactive({ list: [4,5,6] }) // 错误:重新赋值给变量本身,丢失响应式

解决:永远通过属性修改,或用 ref 包装对象,因为 ref 允许整体替换 .value

const state = ref({ list: [] })
state.value = { list: [4,5,6] } // 正确

3. Hooks 内部创建非响应式变量

问题:在 Hook 里定义了普通变量,认为它能像 ref 一样自动触发更新。

function useCounter() {
  let count = 0
  const increment = () => count++
  return { count, increment } // count 永远是 0
}

解决:所有需要驱动视图变化的数据,都必须用 refreactive 包裹。

4. 闭包陷阱:拿到过期的响应式值

问题:在异步回调或 setTimeout 中直接使用解构出来的值,而不是响应式源本身,导致读取的是旧值。

function useTimer() {
  const count = ref(0)
  setTimeout(() => {
    console.log(count.value) // 正确:拿到最新值
    // 但如果写成 const val = count.value; setTimeout(() => console.log(val)) 就是旧值
  }, 1000)
}

利用 watchwatchEffect 来处理副作用,或者在回调中始终通过 .value 访问,不要解构后存为局部变量。

5. 忘记清理副作用(内存泄漏)

问题:Hooks 中注册了定时器、事件监听、订阅,但在组件卸载时没有清除。

function useMouse() {
  const x = ref(0)
  const update = (e) => { x.value = e.pageX }
  window.addEventListener('mousemove', update)
  // 组件卸载时 listener 依然存在
}

解决:利用 onUnmountedwatchEffect/watch 返回的清理函数。

function useMouse() {
  const x = ref(0)
  const update = (e) => { x.value = e.pageX }
  window.addEventListener('mousemove', update)
  onUnmounted(() => window.removeEventListener('mousemove', update))
}

如果使用 watchEffect,可以返回清理函数,Vue 会自动在停止监听时调用。

6. Hook 返回值命名混淆

问题:自定义 Hook 返回一个对象或数组,但没有清晰的命名规范,导致使用时不知哪个是数据、哪个是方法。

// 反例
const [a, b] = useSomething() // 难以理解

解决:尽量返回对象(具名)或使用有意义的数组解构命名(如 const { x, y, reset } = useCoord())。如果必须返回数组,遵循约定(如 [value, setter][data, loading, error])。

7. 在 Hook 内部修改外部传入的响应式对象

问题:将父组件传过来的 reactive 对象通过参数传入 Hook,然后直接修改其属性,破坏了单向数据流,导致状态变更难以追踪。

function useEdit(user) {
  // 错误:直接修改了传入的响应式对象
  user.name = 'new name'
}

解决:如果需要修改外部状态,应该通过回调函数或事件通知,或者内部创建副本再操作。只在明确设计为“双向绑定”的场景(如表单 Hook)才直接修改,并且要文档化。

8. 过度封装与抽象

问题:把很多不相关逻辑挤进一个 Hook,或者为了复用而强行抽象,导致 Hook 参数爆炸,内部判断分支复杂。
解决:保持 Hook 职责单一,遵循“一个 Hook 只做一件事”。如果逻辑确实复杂,可以拆成多个独立 Hook,在组件中自由组合。

9. 忽略 TypeScript 类型导出

问题:自定义 Hook 没有明确的类型标注,使用时没有智能提示和类型校验。
解决:为 Hook 的参数和返回值明确定义 interface/type,并导出,让调用方享受完整类型支持。

10. 响应式依赖缺失导致 watch/watched 不触发

问题:在 watchEffect 内部使用了不可追踪的变量(如普通变量解构),导致依赖没有收集。

const state = reactive({ count: 0 })
const { count } = state
watchEffect(() => {
  console.log(count) // 这里的 count 已经是普通数字,非响应式源
})

解决:直接在回调里使用 state.counttoRef 后访问 .value

最佳实践要点

  • 传入参数尽量使用 refreactive 源的属性引用(toRef,保持响应式链接。
  • 返回对象使用 toRefs 或直接暴露 ref 对象
  • 副作用必须清理,利用 onUnmountedwatchonCleanup
  • 名称以 use 开头,便于编辑器识别和 lint 检查。
  • 编写单元测试,尤其是涉及异步操作和复杂状态变动的 Hook。

遵循这些注意事项,自定义 Hooks 才能真正成为你工具箱里“拿来即用、久用不坏”的可靠零件。