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


## 概述

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

但还有一类重复：**逻辑的重复**。

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

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

命名约定：以 `use` 开头，比如 `useMouse`、`useFetch`。

## 第一个 Composable

追踪鼠标位置的逻辑，封装成 `useMouse`：

```typescript
// 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 }
}
```

在组件中使用：

```vue
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'

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

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

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

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

## 带参数的 Composable

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

```typescript
// 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 }
}
```

在组件中使用：

```vue
<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`），页面会随数据加载自动更新。

## Composable vs Pinia

它们容易混淆，但用途不同：

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

举个例子：

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

> [!tip] 技巧
> 如果一段逻辑只涉及计算和副作用，不涉及跨组件共享，写成 composable。如果需要跨组件共享同一份状态，用 Pinia。

## Composable 的复用规则

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

```typescript
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 相比有几个优势：

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


---

> 作者: Aphros  
> URL: https://blog.papergate.top/posts/%E5%8C%96%E7%B9%81%E4%B8%BA%E7%AE%80%E7%BB%84%E5%90%88%E5%BC%8F%E5%87%BD%E6%95%B0%E5%B0%81%E8%A3%85%E9%80%BB%E8%BE%91/  

