Vue组合式函数(Composables)从入门到实战:鼠标跟踪、请求封装、本地存储……全案例拆解

一、啥是组合式函数?为什么要学它?

Vue 3 的组合式 API(refcomputedwatch 等)让我们能把同一个功能相关的代码写在一起 ,不用再像选项式 API 那样分散在 datamethodscomputed 里。

但这还只是组件内部的写法。如果多个组件都需要同样的功能(比如监听鼠标位置、发起网络请求、读写 localStorage),难道要把那段代码在每个组件里都写一遍吗?

当然不。我们可以把那坨带响应式的逻辑 单独抽出来,放进一个函数里,这个函数就是组合式函数 。通常约定,函数名以 use 开头,比如 useMouseuseFetch

一句话总结: 组合式函数就是"能共享的、带状态的逻辑块"。


二、第一个案例:监听鼠标位置

需求:实时显示鼠标在页面上的坐标。这个功能可能在多个组件里用到,所以我们把它抽成 useMouse

2.1 编写组合式函数 useMouse.js

javascript

复制代码
// useMouse.js
import { ref, onMounted, onBeforeUnmount } from 'vue'

// 定义一个组合式函数,功能是跟踪鼠标位置
export function useMouse() {
  // 用 ref 定义坐标,它们是响应式的
  const x = ref(0)  // 横坐标
  const y = ref(0)  // 纵坐标

  // 鼠标移动时更新坐标
  function handleMouseMove(event) {
    x.value = event.pageX  // pageX 是鼠标相对于整个文档的横坐标
    y.value = event.pageY  // pageY 是纵坐标
  }

  // 组件挂载后,添加全局鼠标移动监听
  onMounted(() => {
    // addEventListener 给 window 绑定 mousemove 事件
    window.addEventListener('mousemove', handleMouseMove)
  })

  // 组件销毁前,移除监听,防止内存泄漏
  onBeforeUnmount(() => {
    window.removeEventListener('mousemove', handleMouseMove)
  })

  // 把需要暴露给外部使用的响应式数据和方法返回出去
  return { x, y }
}

关键点:

  • 函数内部使用 Vue 的 refonMounted 等组合式 API,就像在组件里一样。

  • 它返回一个对象,包含了需要给外部用的响应式数据(xy)。

  • 生命周期钩子在调用它的组件里生效,当组件挂载时绑定事件,销毁时自动移除。

2.2 在组件中使用

vue

复制代码
<template>
  <div>
    <p>鼠标横坐标:{{ mouseX }}</p>
    <p>鼠标纵坐标:{{ mouseY }}</p>
  </div>
</template>

<script setup>
// 引入刚才写的组合式函数
import { useMouse } from './useMouse.js'

// 调用 useMouse,拿到返回的 x 和 y
// 这里用解构赋值,并且可以重命名,防止和其他变量冲突
const { x: mouseX, y: mouseY } = useMouse()
// 现在 mouseX 和 mouseY 就是响应式的,模板里直接用
</script>

效果: 鼠标在页面上移动,坐标实时更新。


三、案例二:监听窗口大小变化

这次我们封装一个 useWindowSize,用于获取浏览器窗口的宽高,窗口大小变化时自动更新。

3.1 useWindowSize.js

javascript

复制代码
// useWindowSize.js
import { ref, onMounted, onBeforeUnmount } from 'vue'

export function useWindowSize() {
  // 初始值设为 0,挂载后更新为真实值
  const width = ref(0)
  const height = ref(0)

  // 更新宽高的方法
  function updateSize() {
    width.value = window.innerWidth   // 窗口宽度
    height.value = window.innerHeight // 窗口高度
  }

  onMounted(() => {
    // 先立即获取一次
    updateSize()
    // 监听窗口大小变化事件
    window.addEventListener('resize', updateSize)
  })

  onBeforeUnmount(() => {
    window.removeEventListener('resize', updateSize)
  })

  return { width, height }
}

3.2 组件中使用

vue

复制代码
<template>
  <div>
    <p>窗口宽度:{{ width }}px</p>
    <p>窗口高度:{{ height }}px</p>
  </div>
</template>

<script setup>
import { useWindowSize } from './useWindowSize.js'

const { width, height } = useWindowSize()
</script>

四、案例三:封装网络请求(fetch)

大部分应用都要发请求。我们来写一个通用的 useFetch,接收一个 URL,返回数据、加载状态和错误信息。

4.1 useFetch.js

javascript

复制代码
// useFetch.js
import { ref, watchEffect, toValue } from 'vue'

// urlOrFn 可以是一个字符串,也可以是一个返回字符串的函数
export function useFetch(urlOrFn) {
  // 响应式数据:存放请求回来的 JSON
  const data = ref(null)
  // 是否正在加载
  const loading = ref(false)
  // 错误信息
  const error = ref(null)

  // watchEffect 会自动追踪依赖,当 url 变化时重新请求
  watchEffect(async () => {
    // 重置状态
    loading.value = true
    data.value = null
    error.value = null

    // 如果是函数就调用获取 url,如果是普通 ref/字符串 就用 toValue 取原始值
    const url = toValue(urlOrFn)

    try {
      const response = await fetch(url)
      // 如果状态码不是 200-299,抛出错误
      if (!response.ok) {
        throw new Error(`请求失败:${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  })

  // 返回数据、加载状态、错误
  return { data, loading, error }
}

4.2 组件中使用

vue

复制代码
<template>
  <div>
    <button @click="userId++">下一个用户(ID: {{ userId }})</button>
    <p v-if="loading">加载中...</p>
    <p v-else-if="error">出错了:{{ error }}</p>
    <div v-else>
      <p>用户名:{{ data?.name }}</p>
      <p>邮箱:{{ data?.email }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useFetch } from './useFetch.js'

const userId = ref(1)

// 根据 userId 动态生成请求 URL
const url = computed(() => `https://jsonplaceholder.typicode.com/users/${userId.value}`)

// 调用 useFetch,传入 computed 生成的 url
const { data, loading, error } = useFetch(url)
</script>

亮点:

  • useFetch 内部用 watchEffect 自动追踪 URL 的变化,URL 一变就重新请求。

  • 返回的 dataloadingerror 都是响应式的,模板直接用。


五、案例四:操作 localStorage

很多场景需要把用户偏好(比如主题、语言)存到本地。我们写一个 useLocalStorage,让它能像 ref 一样读写,并且自动同步到 localStorage。

5.1 useLocalStorage.js

javascript

复制代码
// useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  // 尝试从 localStorage 读取,没读到就用默认值
  const storedValue = localStorage.getItem(key)
  const initialValue = storedValue !== null ? JSON.parse(storedValue) : defaultValue

  // 创建一个响应式 ref
  const data = ref(initialValue)

  // 监听 data 的变化,自动写回 localStorage
  watch(data, (newValue) => {
    // 如果值为 null 或 undefined,就删除该 key
    if (newValue === null || newValue === undefined) {
      localStorage.removeItem(key)
    } else {
      localStorage.setItem(key, JSON.stringify(newValue))
    }
  }, { deep: true }) // deep: true 可以监听到对象内部的变化

  return data
}

5.2 组件中使用

vue

复制代码
<template>
  <div>
    <p>你的名字:{{ name }}</p>
    <input v-model="name" placeholder="输入名字,刷新页面还在" />
    <button @click="name = null">清除名字</button>
  </div>
</template>

<script setup>
import { useLocalStorage } from './useLocalStorage.js'

// 就像使用一个普通的 ref,但会自动存到 localStorage
const name = useLocalStorage('user-name', '小明')
</script>

效果: 输入名字后刷新页面,输入框里的值还在,因为已经存到了 localStorage 里。


六、案例五:防抖函数

防抖是前端常用优化手段:用户连续输入时不要立刻触发搜索,而是等停止输入一段时间后再执行。我们写一个 useDebounce

6.1 useDebounce.js

javascript

复制代码
// useDebounce.js
import { ref, watch } from 'vue'

// sourceRef 是要防抖的源数据(ref),delay 是延迟时间(毫秒)
export function useDebounce(sourceRef, delay = 500) {
  // 存放防抖后的值
  const debouncedValue = ref(sourceRef.value)

  let timer = null

  // 监听源数据的变化
  watch(sourceRef, (newValue) => {
    // 每次变化先清除上一次的定时器
    clearTimeout(timer)
    // 设置新的定时器
    timer = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })

  return debouncedValue
}

6.2 组件中使用

vue

复制代码
<template>
  <div>
    <input v-model="keyword" placeholder="输入搜索关键词" />
    <p>实时输入:{{ keyword }}</p>
    <p>防抖后(500ms):{{ debouncedKeyword }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useDebounce } from './useDebounce.js'

const keyword = ref('')
const debouncedKeyword = useDebounce(keyword, 500)
// 后续发请求就可以用 debouncedKeyword 代替 keyword
</script>

七、综合实战案例:一个带防抖搜索的用户列表

我们把前面学到的几个组合式函数揉在一起,做一个完整的功能:有一个搜索框,用户输入关键词,500ms 防抖后去请求一个模拟的 API,展示过滤后的用户列表。

7.1 模拟 API 函数

为了方便演示,先写一个模拟搜索的函数。

javascript

复制代码
// api.js
// 模拟用户数据
const users = [
  { id: 1, name: '小明' },
  { id: 2, name: '小红' },
  { id: 3, name: '小刚' },
  { id: 4, name: '小丽' },
]

// 模拟异步搜索,延迟 300ms 返回匹配的用户
export function searchUsers(keyword) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const result = users.filter(user => user.name.includes(keyword))
      resolve(result)
    }, 300)
  })
}

7.2 自定义组合式函数 useUserSearch.js

javascript

复制代码
// useUserSearch.js
import { ref, watch } from 'vue'
import { searchUsers } from './api.js'
import { useDebounce } from './useDebounce.js'

export function useUserSearch() {
  // 用户输入的关键词
  const keyword = ref('')
  // 防抖后的关键词
  const debouncedKeyword = useDebounce(keyword, 500)
  // 搜索结果
  const result = ref([])
  // 加载状态
  const loading = ref(false)

  // 当防抖后的关键词变化时,执行搜索
  watch(debouncedKeyword, async (newKeyword) => {
    if (!newKeyword.trim()) {
      result.value = []
      return
    }
    loading.value = true
    result.value = await searchUsers(newKeyword)
    loading.value = false
  })

  return { keyword, result, loading }
}

7.3 组件中使用

vue

复制代码
<template>
  <div>
    <h2>用户搜索</h2>
    <input v-model="keyword" placeholder="输入用户名称" />
    <p v-if="loading">搜索中...</p>
    <ul v-else>
      <li v-for="user in result" :key="user.id">{{ user.name }}</li>
    </ul>
    <p v-if="!loading && keyword && result.length === 0">无结果</p>
  </div>
</template>

<script setup>
import { useUserSearch } from './useUserSearch.js'

const { keyword, result, loading } = useUserSearch()
</script>

代码拆解:

  • useUserSearch 把搜索相关的所有逻辑(防抖、请求、状态管理)全部封装了起来。

  • 组件内部只需要调用它,拿到 keywordresultloading,然后纯展示。

  • 以后其他地方需要搜索功能,直接复用 useUserSearch 就行,代码完全不用重写。


八、总结

今天我们学习了组合式函数,它是 Vue 3 复用逻辑的核心手段。

编写套路:

  1. 导出一个以 use 开头的函数。

  2. 函数内部可以使用 refcomputedwatch、生命周期等所有组合式 API。

  3. 返回需要给外部使用的响应式数据和方法。

什么时候用?

  • 多个组件需要同一段有状态的逻辑时。

  • 把组件里的复杂逻辑抽出来,让组件更干净。

常见命名:

  • useMouseuseFetchuseLocalStorageuseDebounce 等。

学会了组合式函数,你就真正迈入了"中高级前端"的大门。以后维护项目、写新功能,都会轻松不止一倍。

有问题评论区说,看到就回。下篇咱们可以聊聊 Vue 与 TypeScript 的集成,或者你想听什么也可以点播!

相关推荐
Upsy-Daisy1 小时前
Hermes Agent 学习笔记 02:安装、配置与第一次运行
java·前端·数据库
一壶纱1 小时前
一个用于 UniApp 项目的 Pinia 持久化插件
前端·javascript·vue.js
凌涘1 小时前
JS 八大基本类型:一场内存视角的冒险之旅
前端·javascript
心之所向vjuif1 小时前
使用 Gemini 解决前端代码报错问题
前端
San813_LDD2 小时前
[深度学习] 数据序列化格式对比:以日志级别配置为例
xml·java·前端
2601_949695592 小时前
昨天刚解决:说说我是怎么修好Realtek高清晰音频管理器打不开的
驱动开发·计算机外设·电脑
永远的WEB小白2 小时前
css改变svg图标的颜色
前端·javascript·css
lfwh2 小时前
探针程序技术解析:基于 Spring Boot 非 Web 模式的云服务监控告警系统
前端·spring boot·后端
Ajie'Blog2 小时前
AI 周报 | Claude Opus 4.8、Copilot Agent 和 Codex 工作流加速
前端·人工智能·gpt·ai·copilot·ai编程