Home
avatar

TheBlockSky

Vue 3 Composition API 实战指南

为什么需要 Composition API?

Vue 2 的 Options API(选项式 API)在组件逻辑复杂时,会面临一个经典问题——逻辑分散。同一个功能的代码被分散在 datamethodscomputedwatch 等不同选项中,导致代码难以维护和复用。

Composition API(组合式 API)通过一组全新的 API,让我们可以按逻辑关注点组织代码,而不是按选项类型。

<script setup>
// Options API:同一功能的代码分散在各处
export default {
  data() {
    return { count: 0, doubled: 0 }
  },
  methods: {
    increment() { this.count++ }
  },
  watch: {
    count(val) { this.doubled = val * 2 }
  }
}
</script>

<script setup>
// Composition API:按逻辑组织
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() { count.value++ }
</script>

核心 API

ref 与 reactive

refreactive 是 Vue 3 中创建响应式数据的两个核心 API:

<script setup>
import { ref, reactive } from 'vue'

// ref:适用于基本类型和需要重新赋值的场景
const count = ref(0)
const message = ref('hello')

// reactive:适用于对象类型
const user = reactive({
  name: 'Alice',
  age: 25,
  hobbies: ['coding', 'reading']
})

// 修改数据
function updateUser() {
  count.value++  // ref 需要通过 .value 访问
  user.age = 26  // reactive 直接访问
}
</script>

<template>
  <p>{{ count }}</p>
  <p>{{ user.name }} - {{ user.age }}</p>
</template>

computed

计算属性用于派生状态:

<script setup>
import { ref, computed } from 'vue'

const todos = ref([
  { text: '学习 Vue 3', done: true },
  { text: '写博客', done: false },
  { text: '锻炼身体', done: false }
])

// 只读计算属性
const completedCount = computed(() =>
  todos.value.filter(t => t.done).length
)

// 可写计算属性
const allDone = computed({
  get() {
    return todos.value.every(t => t.done)
  },
  set(value) {
    todos.value.forEach(t => { t.done = value })
  }
})
</script>

watch 与 watchEffect

侦听器用于响应数据变化执行副作用:

<script setup>
import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const results = ref([])

// watch:监听特定数据源
watch(searchQuery, async (newVal, oldVal) => {
  if (newVal.length > 2) {
    results.value = await fetchSearch(newVal)
  }
}, { debounce: 300 })

// watchEffect:自动追踪依赖
watchEffect(() => {
  // 会自动追踪内部使用的响应式数据
  console.log(`当前搜索: ${searchQuery.value}`)
  document.title = `搜索: ${searchQuery.value}`
})
</script>

逻辑复用:Composables

Composition API 最大的优势之一就是逻辑复用。我们可以将可复用的逻辑提取为 composable 函数:

示例:useMouse

// 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.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

在组件中使用:

<script setup>
import { useMouse } from './composables/useMouse'

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

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

示例:useLocalStorage

// composables/useLocalStorage.ts
import { ref, watch } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)

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

  return data
}

使用:

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

const theme = useLocalStorage('theme', 'light')
const settings = useLocalStorage('settings', {
  fontSize: 14,
  language: 'zh-CN'
})

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>

实战:Todo List

结合以上知识,实现一个完整的 Todo List 组件:

<script setup>
import { ref, computed } from 'vue'

// 状态
const todos = ref([
  { id: 1, text: '学习 Composition API', done: true },
  { id: 2, text: '写一个 composable', done: false },
])
const newTodo = ref('')
const filter = ref('all')

// 计算属性
const filteredTodos = computed(() => {
  switch (filter.value) {
    case 'active': return todos.value.filter(t => !t.done)
    case 'completed': return todos.value.filter(t => t.done)
    default: return todos.value
  }
})

const activeCount = computed(() =>
  todos.value.filter(t => !t.done).length
)

// 方法
function addTodo() {
  if (!newTodo.value.trim()) return
  todos.value.push({
    id: Date.now(),
    text: newTodo.value.trim(),
    done: false
  })
  newTodo.value = ''
}

function toggleTodo(id: number) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.done = !todo.done
}

function removeTodo(id: number) {
  todos.value = todos.value.filter(t => t.id !== id)
}
</script>

<template>
  <div class="todo-app">
    <h2>Todo List ({{ activeCount }} 项未完成)</h2>

    <input
      v-model="newTodo"
      @keyup.enter="addTodo"
      placeholder="添加新任务..."
    />

    <div class="filters">
      <button @click="filter = 'all'">全部</button>
      <button @click="filter = 'active'">进行中</button>
      <button @click="filter = 'completed'">已完成</button>
    </div>

    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.done"
          @change="toggleTodo(todo.id)"
        />
        <span :class="{ done: todo.done }">{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

生命周期钩子

Composition API 中的生命周期钩子:

<script setup>
import {
  onMounted,
  onUnmounted,
  onBeforeMount,
  onUpdated,
  onBeforeUpdate,
  onActivated,
  onDeactivated
} from 'vue'

onBeforeMount(() => {
  console.log('组件挂载前')
})

onMounted(() => {
  console.log('组件已挂载')
  // 适合:API 请求、DOM 操作、事件监听
})

onBeforeUpdate(() => {
  console.log('组件更新前')
})

onUpdated(() => {
  console.log('组件已更新')
})

onUnmounted(() => {
  console.log('组件已卸载')
  // 适合:清理定时器、移除事件监听
})
</script>

最佳实践

1. 使用 <script setup>

<script setup> 是 Composition API 的语法糖,推荐在所有新项目中使用:

<script setup>
// 无需 export default
// 顶层的绑定会自动暴露给模板
import { ref } from 'vue'

const count = ref(0)
</script>

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

2. 合理拆分 composable

  • 一个 composable 只做一件事
  • 使用明确的命名约定(useXxx)
  • 返回对象而不是直接修改外部状态

3. 类型安全

结合 TypeScript 获得更好的类型推导:

interface User {
  id: number
  name: string
  email: string
}

export function useUser(id: Ref<number>) {
  const user = ref<User | null>(null)
  const loading = ref(false)

  watch(id, async (newId) => {
    loading.value = true
    user.value = await fetchUser(newId)
    loading.value = false
  }, { immediate: true })

  return { user, loading }
}

总结

Composition API 不是要取代 Options API,而是提供了一种更灵活、更强大的代码组织方式。它特别适合:

  1. 复杂组件:逻辑按功能聚合,而非按选项分散
  2. 逻辑复用:通过 composable 轻松共享状态逻辑
  3. 类型推导:与 TypeScript 配合得天衣无缝
  4. Tree-shaking:按需导入,减小打包体积

从 Options API 到 Composition API 的转变,不仅是 API 的变化,更是一种思维方式的升级——从”我的数据在哪里”到”我的功能在哪里”。

Vue Composition API 前端 框架