发布于

Vue 3 Composition API 完全指南:响应式编程新范式

作者

Vue 3 Composition API 完全指南:响应式编程新范式

Vue 3 引入的 Composition API 为我们提供了一种全新的组件逻辑组织方式。本文将深入探讨 Composition API 的核心概念、使用方法和最佳实践。

为什么需要 Composition API?

Options API 的局限性

在 Vue 2 中,我们使用 Options API 来组织组件逻辑:

// Vue 2 Options API
export default {
  data() {
    return {
      count: 0,
      user: null,
      loading: false,
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    },
  },
  methods: {
    increment() {
      this.count++
    },
    async fetchUser() {
      this.loading = true
      try {
        this.user = await api.getUser()
      } finally {
        this.loading = false
      }
    },
  },
  mounted() {
    this.fetchUser()
  },
}

当组件变得复杂时,相关的逻辑会分散在不同的选项中,难以维护。

Composition API 的优势

// Vue 3 Composition API
import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    // 计数器逻辑
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    const increment = () => count.value++

    // 用户数据逻辑
    const user = ref(null)
    const loading = ref(false)

    const fetchUser = async () => {
      loading.value = true
      try {
        user.value = await api.getUser()
      } finally {
        loading.value = false
      }
    }

    onMounted(fetchUser)

    return {
      count,
      doubleCount,
      increment,
      user,
      loading,
      fetchUser,
    }
  },
}

核心 API 详解

1. 响应式 API

ref:基础响应式引用

import { ref, isRef, unref } from 'vue'

// 创建响应式引用
const count = ref(0)
const message = ref('Hello')

// 访问和修改值
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

// 类型检查和解包
console.log(isRef(count)) // true
console.log(unref(count)) // 1 (等同于 count.value)

// 模板中自动解包
// <template>{{ count }}</template> // 不需要 .value

reactive:深层响应式对象

import { reactive, isReactive, toRefs } from 'vue'

// 创建响应式对象
const state = reactive({
  count: 0,
  user: {
    name: 'John',
    age: 30,
  },
  todos: [],
})

// 直接访问属性
state.count++
state.user.name = 'Jane'
state.todos.push({ id: 1, text: 'Learn Vue 3' })

// 解构响应式对象
const { count, user } = toRefs(state)
// 现在 count 和 user 都是 ref

computed:计算属性

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算属性
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(value) {
    ;[firstName.value, lastName.value] = value.split(' ')
  },
})

fullNameWritable.value = 'Jane Smith'
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'

watch 和 watchEffect:侦听器

import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// 侦听单个源
watch(count, (newValue, oldValue) => {
  console.log(`count changed from ${oldValue} to ${newValue}`)
})

// 侦听多个源
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
  console.log('Multiple values changed')
})

// 立即执行的侦听器
watchEffect(() => {
  console.log(`Count is ${count.value}`)
  console.log(`Message is ${message.value}`)
})

// 停止侦听
const stopWatcher = watch(count, () => {
  // 侦听逻辑
})

// 在某个时机停止侦听
stopWatcher()

2. 生命周期钩子

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

export default {
  setup() {
    onBeforeMount(() => {
      console.log('组件即将挂载')
    })

    onMounted(() => {
      console.log('组件已挂载')
    })

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

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

    onBeforeUnmount(() => {
      console.log('组件即将卸载')
    })

    onUnmounted(() => {
      console.log('组件已卸载')
    })

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

组合式函数(Composables)

1. 基础组合式函数

// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => (count.value = initialValue)

  const isEven = computed(() => count.value % 2 === 0)
  const isPositive = computed(() => count.value > 0)

  return {
    count,
    increment,
    decrement,
    reset,
    isEven,
    isPositive,
  }
}

// 在组件中使用
import { useCounter } from '@/composables/useCounter'

export default {
  setup() {
    const { count, increment, decrement, isEven } = useCounter(10)

    return {
      count,
      increment,
      decrement,
      isEven,
    }
  },
}

2. 异步数据获取

// composables/useFetch.js
import { ref, isRef, unref, watchEffect } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

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

    try {
      const response = await fetch(unref(url))
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  if (isRef(url)) {
    // 如果 URL 是响应式的,监听其变化
    watchEffect(fetchData)
  } else {
    // 如果 URL 是静态的,直接获取数据
    fetchData()
  }

  return {
    data,
    error,
    loading,
    refetch: fetchData,
  }
}

// 使用示例
export default {
  setup() {
    const userId = ref(1)
    const userUrl = computed(() => `/api/users/${userId.value}`)

    const { data: user, loading, error, refetch } = useFetch(userUrl)

    return {
      user,
      loading,
      error,
      refetch,
      userId,
    }
  },
}

3. 本地存储组合式函数

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

export function useLocalStorage(key, defaultValue) {
  const storedValue = localStorage.getItem(key)
  const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue

  const value = ref(initialValue)

  // 监听值的变化,同步到 localStorage
  watch(
    value,
    (newValue) => {
      if (newValue === null || newValue === undefined) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newValue))
      }
    },
    { deep: true }
  )

  // 监听 storage 事件,同步其他标签页的变化
  window.addEventListener('storage', (e) => {
    if (e.key === key && e.newValue !== null) {
      value.value = JSON.parse(e.newValue)
    }
  })

  return value
}

// 使用示例
export default {
  setup() {
    const theme = useLocalStorage('theme', 'light')
    const userPreferences = useLocalStorage('userPreferences', {
      language: 'zh-CN',
      notifications: true,
    })

    const toggleTheme = () => {
      theme.value = theme.value === 'light' ? 'dark' : 'light'
    }

    return {
      theme,
      userPreferences,
      toggleTheme,
    }
  },
}

高级模式

1. 依赖注入

// 父组件
import { provide, ref } from 'vue'

export default {
  setup() {
    const theme = ref('light')
    const user = ref({ name: 'John', role: 'admin' })

    // 提供数据
    provide('theme', theme)
    provide('user', user)

    return {
      theme,
      user
    }
  }
}

// 子组件
import { inject } from 'vue'

export default {
  setup() {
    // 注入数据
    const theme = inject('theme')
    const user = inject('user')

    // 带默认值的注入
    const config = inject('config', { api: '/api' })

    return {
      theme,
      user,
      config
    }
  }
}

2. 模板引用

import { ref, onMounted } from 'vue'

export default {
  setup() {
    const inputRef = ref(null)
    const listRef = ref([])

    onMounted(() => {
      // 访问 DOM 元素
      inputRef.value.focus()

      // 访问组件实例
      console.log(listRef.value.length)
    })

    return {
      inputRef,
      listRef,
    }
  },
}

// 模板中使用
// <input ref="inputRef" />
// <li v-for="item in items" :key="item.id" ref="listRef">{{ item.name }}</li>

3. 响应式工具

import { toRef, toRefs, readonly, shallowRef, triggerRef, customRef } from 'vue'

// toRef:为响应式对象的属性创建 ref
const state = reactive({ count: 0, name: 'John' })
const countRef = toRef(state, 'count')

// toRefs:将响应式对象转换为普通对象,每个属性都是 ref
const { count, name } = toRefs(state)

// readonly:创建只读代理
const readonlyState = readonly(state)

// shallowRef:浅层响应式
const shallowState = shallowRef({ nested: { count: 0 } })
shallowState.value = { nested: { count: 1 } } // 触发更新
shallowState.value.nested.count = 2 // 不会触发更新

// 手动触发更新
triggerRef(shallowState)

// 自定义 ref
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => ({
    get() {
      track()
      return value
    },
    set(newValue) {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        value = newValue
        trigger()
      }, delay)
    },
  }))
}

最佳实践

1. 逻辑复用

// 将相关逻辑组织在一起
function useUserManagement() {
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)

  const fetchUsers = async () => {
    loading.value = true
    try {
      users.value = await api.getUsers()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  const addUser = async (userData) => {
    const newUser = await api.createUser(userData)
    users.value.push(newUser)
  }

  const removeUser = async (userId) => {
    await api.deleteUser(userId)
    users.value = users.value.filter((user) => user.id !== userId)
  }

  return {
    users,
    loading,
    error,
    fetchUsers,
    addUser,
    removeUser,
  }
}

2. 类型安全(TypeScript)

import { ref, computed, Ref, ComputedRef } from 'vue'

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

interface UseUserReturn {
  user: Ref<User | null>
  loading: Ref<boolean>
  error: Ref<string | null>
  fetchUser: (id: number) => Promise<void>
  updateUser: (userData: Partial<User>) => Promise<void>
}

export function useUser(): UseUserReturn {
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  const fetchUser = async (id: number): Promise<void> => {
    loading.value = true
    error.value = null

    try {
      const response = await api.getUser(id)
      user.value = response.data
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }

  const updateUser = async (userData: Partial<User>): Promise<void> => {
    if (!user.value) return

    try {
      const response = await api.updateUser(user.value.id, userData)
      user.value = response.data
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Update failed'
    }
  }

  return {
    user,
    loading,
    error,
    fetchUser,
    updateUser,
  }
}

总结

Vue 3 Composition API 为我们提供了更灵活、更强大的组件逻辑组织方式:

  1. 更好的逻辑复用:通过组合式函数实现逻辑的提取和复用
  2. 更好的类型推导:与 TypeScript 的完美结合
  3. 更灵活的代码组织:相关逻辑可以组织在一起
  4. 更好的性能:更精确的依赖追踪

掌握 Composition API,你就能充分发挥 Vue 3 的威力,构建更加优雅和可维护的应用!


Vue 3 Composition API 是现代 Vue 开发的核心,值得我们深入学习和实践。