化繁为简:组合式函数封装逻辑

第 1 节 概述

前一篇讲的 Pinia 解决了"数据共享"的问题。

但还有一类重复:逻辑的重复

比如:多个组件都需要追踪鼠标位置,或者都需要用 fetch 调用 API。每个组件里写一遍同样的逻辑,很啰嗦。

Vue 3 提供了组合式函数(Composable) 来复用逻辑。它其实就是函数,只不过内部用到了 Vue 的响应式 API。

命名约定:以 use 开头,比如 useMouseuseFetch

第 2 节 第一个 Composable

追踪鼠标位置的逻辑,封装成 useMouse

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event: MouseEvent) {
    x.value = event.clientX
    y.value = event.clientY
  }

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

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

  return { x, y }
}

在组件中使用:

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()
</script>

<template>
  <p>鼠标位置{{ x }}, {{ y }}</p>
</template>

这就是 composable 的核心思路:把组件中用到的响应式数据和生命周期逻辑,提取到一个函数里

任何需要鼠标位置的组件,都可以直接引用 useMouse,不用重新写一遍事件监听。

第 3 节 带参数的 Composable

封装一个通用的 API 请求 composable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/composables/useFetch.ts
import { ref, watchEffect } from 'vue'

export function useFetch(url: string) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  watchEffect(async () => {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

在组件中使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'

const { data, loading } = useFetch('/api/articles')
</script>

<template>
  <p v-if="loading">加载中...</p>
  <div v-else v-for="item in data" :key="item.id">
    {{ item.title }}
  </div>
</template>

调用 useFetch 时传入 URL,返回的 data 是响应式的(用了 ref),页面会随数据加载自动更新。

第 4 节 Composable vs Pinia

它们容易混淆,但用途不同:

对比ComposablePinia
本质函数,复用逻辑store,管理共享状态
数据范围只在调用它的组件中有效所有组件共享同一份
使用方式每个组件各自调用所有组件用同一个 useXxxStore()
典型场景鼠标追踪、API 请求、表单逻辑用户信息、购物车、全局配置

举个例子:

  • useMouse() → 每个组件调一次,得到自己的鼠标位置(互不干扰)
  • useUserStore() → 所有组件调,拿到同一个用户对象(保持一致)
技巧

如果一段逻辑只涉及计算和副作用,不涉及跨组件共享,写成 composable。如果需要跨组件共享同一份状态,用 Pinia。

第 5 节 Composable 的复用规则

组合式函数之间可以互相调用。这比 mixin(Vue 2 的旧方案)灵活得多:

1
2
3
4
5
6
export function useUser() {
  const { data: user, loading } = useFetch('/api/user')
  const { x, y } = useMouse() // composable 内部调用另一个 composable

  return { user, loading, mouseX: x, mouseY: y }
}

这与 Vue 2 的 mixin 相比有几个优势:

对比mixincomposable
来源多个 mixin 属性冲突时不知道来自哪每个变量从函数解构,来源明确
命名冲突多个 mixin 可能有同名属性拆解时可以用 : 重命名
参数化不支持动态参数可以接收参数动态调整行为