vue 精选推荐

Vue 3 组合式 API 深度指南

HTMLPAGE 团队
13 分钟阅读

完整讲解 Vue 3 组合式 API,包括基础概念、响应式系统、生命周期、自定义 Hooks 等内容,帮助你掌握现代 Vue 开发。

#Vue 3 #组合式 API #Composition API #响应式 #最佳实践

Vue 3 组合式 API 深度指南

Vue 3 的组合式 API 改变了我们组织 Vue 代码的方式。本文深度讲解其核心概念和实战用法。

1. 为什么要使用组合式 API

选项式 API 的问题

<script>
// ❌ 选项式 API 的问题:
// 1. 逻辑分散在 data、methods、computed 等不同地方
// 2. 难以复用逻辑
// 3. 大型组件难以维护

export default {
  data() {
    return {
      count: 0,
      name: '',
      users: []
    }
  },
  
  computed: {
    double() { return this.count * 2 },
    isAdult() { return this.age >= 18 }
  },
  
  methods: {
    increment() { this.count++ },
    setName(name) { this.name = name },
    async fetchUsers() { /* ... */ }
  },
  
  watch: {
    count(newVal) { console.log('count changed') }
  }
}
</script>

组合式 API 的优势

<script setup lang="ts">
// ✅ 组合式 API 的优势:

// 1. 逻辑聚集在一起
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++

watch(count, (newVal) => {
  console.log('count changed:', newVal)
})

// 2. 易于提取为 composable 复用
// 3. 更好的 TypeScript 支持
// 4. 更小的包体积
// 5. 更灵活的逻辑组织

// 相同功能的代码量通常减少 30-50%
</script>

2. 核心响应式 API

ref: 简单值的响应式

import { ref } from 'vue'

// 创建响应式简单值
const count = ref(0)
const name = ref('Alice')
const isActive = ref(false)

// ❌ 错误: ref 需要 .value 访问
console.log(count)          // { value: 0 }

// ✅ 正确: 在 JavaScript 中需要 .value
console.log(count.value)    // 0

// ✅ 在模板中自动解包
<template>
  {{ count }}               <!-- 自动为 0,无需 .value -->
  {{ count + 1 }}          <!-- 直接计算 -->
</template>

// 响应式更新
const increment = () => {
  count.value++            // 必须使用 .value
}

reactive: 对象的深度响应式

import { reactive } from 'vue'

// 创建响应式对象
const user = reactive({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  address: {
    city: 'Beijing',
    zip: '100000'
  }
})

// 无需 .value,直接访问和修改
console.log(user.name)              // "Alice"
user.name = 'Bob'                   // 更新
user.address.city = 'Shanghai'      // 深度响应式

// ✅ 可以直接解构
const { name, email } = user        // 有效(reactive)

// ❌ 不要对 ref 解构(会失去响应性)
const count = ref(0)
const { value } = count             // ❌ value 不再响应式

computed: 计算属性

import { ref, computed } from 'vue'

const count = ref(0)

// 1. 只读计算属性
const double = computed(() => count.value * 2)
console.log(double.value)  // 0 时为 0

// 2. 可写计算属性
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value: string) => {
    const [first, last] = value.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

fullName.value = 'John Doe'  // 触发 setter

watch: 侦听器

import { ref, watch } from 'vue'

const count = ref(0)
const user = reactive({ name: 'Alice', age: 0 })

// 1. 侦听单个 ref
watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})

// 2. 侦听多个源
watch(
  [count, () => user.name],
  ([newCount, newName], [oldCount, oldName]) => {
    console.log(`count: ${oldCount} → ${newCount}`)
    console.log(`name: ${oldName} → ${newName}`)
  }
)

// 3. 深度侦听 (监听对象的嵌套属性)
watch(
  () => user,
  (newUser) => {
    console.log('user 对象改变:', newUser)
  },
  { deep: true }  // 深度侦听
)

// 4. 立即执行
watch(count, (newVal) => {
  console.log('立即执行,且每次 count 改变时执行')
}, { immediate: true })

// 5. 配置侦听时机
watch(count, (newVal) => {
  console.log('在组件更新后执行')
}, { flush: 'post' })

// 停止侦听
const unwatch = watch(count, () => { /* ... */ })
unwatch()  // 停止侦听

3. 生命周期 Hooks

生命周期钩子对应关系

import {
  onBeforeMount, onMounted,
  onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted,
  onErrorCaptured, onActivated, onDeactivated
} from 'vue'

// 挂载
onBeforeMount(() => console.log('组件挂载前'))
onMounted(() => console.log('组件挂载后'))

// 更新
onBeforeUpdate(() => console.log('组件更新前'))
onUpdated(() => console.log('组件更新后'))

// 卸载
onBeforeUnmount(() => console.log('组件卸载前'))
onUnmounted(() => console.log('组件卸载后'))

// 错误处理
onErrorCaptured((err, instance, info) => {
  console.error('捕获错误:', err)
  return false  // 阻止错误继续传播
})

// keep-alive 激活/停用
onActivated(() => console.log('组件被激活'))
onDeactivated(() => console.log('组件被停用'))

实战: 生命周期最佳实践

export default {
  setup() {
    // 所有响应式状态
    const data = ref('')
    const loading = ref(false)
    const error = ref<string | null>(null)
    
    // 获取数据
    const fetchData = async () => {
      loading.value = true
      try {
        const response = await fetch('/api/data')
        data.value = await response.text()
      } catch (e) {
        error.value = e.message
      } finally {
        loading.value = false
      }
    }
    
    // 组件挂载时获取数据
    onMounted(() => {
      fetchData()
    })
    
    // 组件卸载时清理
    onUnmounted(() => {
      // 取消请求、清除定时器等
      data.value = null
    })
    
    // 返回模板需要的数据
    return {
      data,
      loading,
      error
    }
  }
}

4. 自定义 Composable (可复用逻辑)

创建 Composable 的规范

// ✅ composables/useCounter.ts

import { ref, computed } from 'vue'

export const useCounter = (initialValue: number = 0) => {
  // 内部状态(私有)
  const count = ref(initialValue)
  
  // 暴露的计算属性
  const double = computed(() => count.value * 2)
  const isEven = computed(() => count.value % 2 === 0)
  
  // 暴露的方法
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  // 返回暴露的 API
  return {
    count,
    double,
    isEven,
    increment,
    decrement,
    reset
  }
}

// 在组件中使用
<script setup lang="ts">
const { count, double, increment, reset } = useCounter(10)
</script>

<template>
  <div>
    <p>{{ count }} (double: {{ double }})</p>
    <button @click="increment">+1</button>
    <button @click="reset">Reset</button>
  </div>
</template>

实战案例 1: 数据获取

// composables/useFetch.ts

export const useFetch = (url: string) => {
  const data = ref(null)
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  const fetch = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await $fetch(url)
      data.value = response
    } catch (e: any) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }
  
  // 组件挂载时自动获取
  onMounted(() => fetch())
  
  // 允许手动刷新
  const refresh = () => fetch()
  
  return {
    data,
    loading,
    error,
    refresh
  }
}

// 使用
<script setup lang="ts">
const { data: users, loading, error, refresh } = useFetch('/api/users')
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error" class="error">{{ error }}</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
  <button @click="refresh">刷新</button>
</template>

实战案例 2: 表单处理

// composables/useForm.ts

export const useForm = (
  initialValues: Record<string, any>,
  onSubmit: (values: Record<string, any>) => Promise<void>
) => {
  const form = reactive({ ...initialValues })
  const errors = reactive<Record<string, string>>({})
  const isSubmitting = ref(false)
  
  const validate = () => {
    // 简单验证示例
    if (!form.email?.includes('@')) {
      errors.email = '请输入有效的邮箱'
      return false
    }
    return true
  }
  
  const handleSubmit = async () => {
    if (!validate()) return
    
    isSubmitting.value = true
    try {
      await onSubmit(form)
    } catch (e: any) {
      errors.submit = e.message
    } finally {
      isSubmitting.value = false
    }
  }
  
  const reset = () => {
    Object.assign(form, initialValues)
    Object.keys(errors).forEach(key => delete errors[key])
  }
  
  return {
    form,
    errors,
    isSubmitting,
    handleSubmit,
    reset
  }
}

// 使用
<script setup lang="ts">
const { form, errors, isSubmitting, handleSubmit, reset } = useForm(
  { email: '', password: '' },
  async (values) => {
    await loginUser(values)
  }
)
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.email" type="email" />
    <span v-if="errors.email" class="error">{{ errors.email }}</span>
    
    <input v-model="form.password" type="password" />
    
    <button :disabled="isSubmitting" type="submit">
      {{ isSubmitting ? '提交中...' : '提交' }}
    </button>
    <button type="button" @click="reset">重置</button>
  </form>
</template>

实战案例 3: 事件处理

// composables/useEventListener.ts

export const useEventListener = (
  target: any,
  event: string,
  callback: (event: Event) => void
) => {
  onMounted(() => {
    target.addEventListener(event, callback)
  })
  
  onUnmounted(() => {
    target.removeEventListener(event, callback)
  })
}

// 使用 - 窗口调整大小
<script setup lang="ts">
const windowWidth = ref(window.innerWidth)

useEventListener(window, 'resize', () => {
  windowWidth.value = window.innerWidth
})
</script>

5. 高级用法

Composable 中使用其他 Composable

// ✅ 完全支持 composable 组合

// composables/useUser.ts
export const useUser = (userId: string) => {
  const { data: user } = useFetch(`/api/users/${userId}`)
  
  const updateUser = async (newData: any) => {
    user.value = { ...user.value, ...newData }
    await $fetch(`/api/users/${userId}`, {
      method: 'PUT',
      body: newData
    })
  }
  
  return { user, updateUser }
}

// composables/useUserProfile.ts
export const useUserProfile = (userId: string) => {
  // 使用其他 composable
  const { user, updateUser } = useUser(userId)
  const { data: posts } = useFetch(`/api/users/${userId}/posts`)
  
  return { user, posts, updateUser }
}

响应式副作用

// watchEffect: 自动追踪依赖
import { watchEffect } from 'vue'

const userId = ref(1)
const user = ref(null)

watchEffect(async () => {
  // userId 改变时自动重新执行
  const response = await fetch(`/api/users/${userId.value}`)
  user.value = await response.json()
})

// userId.value = 2  // 自动触发新的获取

6. 最佳实践

// ✅ Composable 最佳实践

// 1. 命名约定:use 前缀
export const useCounter = () => { /* ... */ }
export const useFetch = (url: string) => { /* ... */ }

// 2. 参数化 composables
export const useFetch = (url: string | Ref<string>) => {
  // 支持响应式的 url
}

// 3. 返回响应式对象
export const useForm = () => {
  return {
    form,         // 响应式状态
    errors,       // 响应式状态
    submit,       // 方法
    reset         // 方法
  }
}

// 4. 处理加载和错误
const { data, loading, error, refresh } = useFetch('/api')

// 5. 避免副作用污染
export const useGoodComposable = () => {
  // ✅ 好:副作用在组件生命周期内管理
  onMounted(() => {
    // 初始化
  })
  
  onUnmounted(() => {
    // 清理
  })
}

// ❌ 避免:直接执行副作用
export const useBadComposable = () => {
  // ❌ 不好:每次导入都执行
  console.log('这会每次都执行')
}

总结

特性优势
ref/reactive灵活的响应式管理
computed派生状态和缓存
watch灵活的侦听机制
生命周期精确的时机控制
Composable逻辑复用和组织
TypeScript完整的类型支持

相关资源