vue3.x教程-自定义 Hooks

介绍

自定义 Hook 就是利用 Vue3 组合式 API(ref/reactive/watch/onMounted 等)封装逻辑的普通函数。

⚠️ 区别:自定义 Hook ≠ 组件,没有模板,只封装逻辑与状态

作用

抽离复用组件逻辑,解决选项式 API 逻辑分散、难以复用的痛点

命名规范

以 use 开头(约定俗成,便于识别),例如 useMouse、useRequest

使用

基于 <script setup> / setup() 组合式 API,只能在setup 执行阶段调用

应用场景(关键)

满足以下条件之一,就应该使用 Hooks:

  1. 需要多处复用
  2. 依赖组件生命周期(onMounted /onUnmounted、事件监听、定时器、请求、订阅等副作用清理)
  3. 函数内部想要直接调用 useXXXStore()(如:Pinia状态管理)

不适合 Hooks

如果,某些功能函数, 仅在当前页面中使用,不复用;或者 不需要生命周期、事件监听等功能;或者不需要在函数内部调用 useXXXStore(),那么只封装成 普通的 .ts 文件 就够用了

规则(必须遵守)

  1. 只能在 <script setup> 内部 或其他自定义 Hook 中调用
  2. 不能在普通函数、条件判断、循环、异步回调里调用(遵循组合式API调用顺序规则)
    // ❌ 错误示例
    function fetchData() {
    const { data } = useRequest() // 禁止在普通函数内部调用hook
    }
    // ✅ 正确
    const { data } = useRequest()
    function fetchData() {}
    
  3. Hook 内部可以使用所有组合 API:ref、reactive、computed、watch、生命周期
  4. 通过 return 向外暴露状态和方法;想要外部修改响应式,直接返回响应式变量

目录结构

项目统一存放 hooks,推荐目录:

src/
└── hooks(或composables)/
    ├── useCounter.js
    ├── useWindowSize.js
    ├── useRequest.js    # 请求封装
    ├── useStorage.js    # localStorage
    └── index.js         # 统一导出

index.js 统一导出:

export * from './useCounter'
export * from './useWindowSize'

案例

实现简单计数器 Hook

创建hooks文件

创建文件:hooks/useCounter.js,内容如下:

import { ref } from 'vue'

// 自定义Hook
export function useCounter(initialValue = 0) {
  // 内部状态(封装私有数据)
  const count = ref(initialValue)

  // 内部方法
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue

  // 向外暴露(响应式数据直接返回)
  return {
    count,
    increment,
    decrement,
    reset
  }
}

使用hooks

创建 view,内容如下:

<script setup>
import { useCounter } from '@/hooks/useCounter'
// 解构拿到响应式变量和方法
const { count, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
</template>

响应式传递要点

1. 返回 ref / reactive → 外部具备响应式(推荐)

export function useDemo() {
  const name = ref('张三')
  return { name }
}

组件中修改 name.value 会双向生效。

2. 如果直接返回普通值 → 丢失响应式

// ❌ 错误
return {
  name: name.value 
}

3. 限制外部修改:封装只读状态

使用 readonly 保护内部状态,只开放方法修改

import { ref, readonly } from 'vue'
export function useCounter() {
  const count = ref(0)
  const add = () => count.value++
  return {
    count: readonly(count), // 外部不能直接修改count.value
    add
  }
}

Hook 中使用生命周期、watch

示例:监听窗口大小 useWindowSize

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

export function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)

  const update = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }

  onMounted(() => {
    window.addEventListener('resize', update)
  })

  onUnmounted(() => {
    window.removeEventListener('resize', update)
  })

  return { width, height }
}

关键优势:自动生命周期销毁,组件卸载自动清除事件,不用每个组件重复写解绑。

参数传递:支持传入响应式参数

Hook 可以接收 ref 响应式参数,内部可用 watch 监听

import { ref, watch } from 'vue'

export function useDouble(val) {
  const doubleVal = ref(val.value * 2)
  watch(val, (v) => {
    doubleVal.value = v * 2
  }, { immediate: true })
  return { doubleVal }
}

组件调用:

const num = ref(10)
const { doubleVal } = useDouble(num)
num.value = 20 // 自动触发hook内部watch更新

组合多个 Hook(Hook 互相调用)

Hook 可以引入其他 Hook,实现逻辑分层

// useMouse.js
export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  // 监听鼠标移动...
  return { x, y }
}

// useMouseDistance.js
import { useMouse } from './useMouse'
import { computed } from 'vue'
export function useMouseDistance() {
  const { x, y } = useMouse()
  const distance = computed(() => {
    return Math.sqrt(x.value ** 2 + y.value ** 2)
  })
  return { distance, x, y }
}

常见实战场景 Hook 模板

1. useStorage 本地存储

import { ref, watch } from 'vue'
export function useStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(data, (val) => {
    localStorage.setItem(key, JSON.stringify(val))
  }, { deep: true })

  return data
}

2. useRequest 简易请求封装

import { ref } from 'vue'
export function useRequest(apiFn) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const run = async (...args) => {
    loading.value = true
    error.value = null
    try {
      data.value = await apiFn(...args)
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  return { data, loading, error, run }
}

常见坑与注意事项

  1. 不要在条件/异步里调用 Hook
    // ❌
    if (flag.value) {
    const { count } = useCounter()
    }
    
  2. 多个组件同时调用同一个 Hook,状态相互隔离
    每次调用 useCounter() 都会创建全新的 ref,组件之间互不干扰;
    如果想要全局共享状态,要配合 pinia 或者单独导出一个外部 ref。

  3. Hook 不是单例!

    const a = useCounter()
    const b = useCounter()
    // a.count 和 b.count 相互独立
    
  4. 全局共享逻辑方案

  • 组件内复用逻辑 → 自定义Hook
  • 跨组件共享状态 → Pinia + Hook
  1. <script setup> 与 setup() 选项均可使用自定义Hook
    // 选项式setup写法也支持
    setup() {
    const { count } = useCounter()
    return { count }
    }
    

自定义Hook vs Mixin 对比

特性 自定义Hook Mixin
命名冲突 无,自由解构 容易命名冲突
数据来源清晰 一眼看出来自哪个hook 来源模糊
按需引入 按需导入 全部合并
TS类型支持 完美支持 类型体验差

Vue3 推荐全部使用自定义Hook替代Mixin


原文出处:/show_1GW3iZpCpZMv.html