Vue 3的Composition API是Vue.js框架的一次重大革新,它提供了一种全新的组件逻辑组织方式,使代码更加模块化、可复用,并且更好地支持TypeScript。本文将深入探讨Composition API的设计理念、核心特性以及在实际项目中的应用。

一、Composition API简介

1. 什么是Composition API?

Composition API是Vue 3引入的一套新的API,它允许开发者以函数式的方式组织组件逻辑,而不是依赖于传统的Options API(data、methods、computed等选项)。

传统Options API vs Composition API:

// Options API
export default {
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  watch: {
    count(newVal) {
      console.log('Count changed:', newVal)
    }
  }
}

// Composition API
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const message = ref('Hello')
    
    function increment() {
      count.value++
    }
    
    const doubleCount = computed(() => count.value * 2)
    
    watch(count, (newVal) => {
      console.log('Count changed:', newVal)
    })
    
    return {
      count,
      message,
      increment,
      doubleCount
    }
  }
}

2. 为什么需要Composition API?

Composition API解决了Options API在以下几个方面的不足:

  • 逻辑复用:Options API通过mixins实现逻辑复用,但存在命名冲突、来源不明确等问题
  • TypeScript支持:Options API在TypeScript下类型推断不够完善
  • 代码组织:当组件变得复杂时,相关逻辑被分散在不同选项中,难以维护
  • 树摇(Tree-shaking):Composition API的函数式设计更容易被打包工具优化

二、Composition API核心函数

1. setup函数

setup是Composition API的入口函数,它在组件实例创建之前执行。

基本用法:

export default {
  setup(props, context) {
    // props是响应式的,不能直接解构
    console.log(props.message)
    
    // context包含attrs、slots、emit等
    const { attrs, slots, emit, expose } = context
    
    // 向父组件暴露方法
    expose({
      reset() {
        // 重置逻辑
      }
    })
    
    // 返回的内容将暴露给模板
    return {
      count: ref(0),
      increment() { /* ... */ }
    }
  }
}

2. 响应式API

ref

创建一个响应式的引用对象。

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

reactive

创建一个响应式的对象。

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: 'John', age: 30 }
})

console.log(state.count) // 0
state.count++ // 直接修改,不需要.value

ref vs reactive

  • ref:适用于基本类型和对象,需要通过.value访问
  • reactive:仅适用于对象,直接访问属性,但不能解构(会失去响应性)

使用toRefs保持响应性:

import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'John' })
const { count, name } = toRefs(state)

// 现在可以解构使用,仍然保持响应性
console.log(count.value) // 0

3. 计算属性与侦听器

computed

创建一个计算属性,具有缓存特性。

import { ref, computed } from 'vue'

const count = ref(0)
const doubleCount = computed(() => count.value * 2)

console.log(doubleCount.value) // 0
count.value++
console.log(doubleCount.value) // 2

可写的计算属性:

const doubleCount = computed({
  get: () => count.value * 2,
  set: (value) => {
    count.value = value / 2
  }
})

doubleCount.value = 10
console.log(count.value) // 5

watch

侦听一个或多个响应式数据的变化。

import { ref, watch } from 'vue'

const count = ref(0)

// 基本用法
watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`)
})

// 侦听多个源
const name = ref('John')
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('Count or name changed')
})

// 深度侦听
const state = ref({ count: 0, user: { name: 'John' } })
watch(state, (newState) => {
  console.log('State changed:', newState)
}, { deep: true })

// 立即执行
watch(count, (newVal) => {
  console.log('Count:', newVal)
}, { immediate: true })

watchEffect

自动跟踪回调函数中使用的响应式数据。

import { ref, watchEffect } from 'vue'

const count = ref(0)
const name = ref('John')

const stop = watchEffect(() => {
  console.log(`Count: ${count.value}, Name: ${name.value}`)
})

// 停止侦听
stop()

4. 生命周期钩子

Composition API中的生命周期钩子以函数形式提供,需要从vue中导入。

import { onMounted, onUpdated, onUnmounted } from 'vue'

export default {
  setup() {
    onMounted(() => {
      console.log('Component mounted')
    })
    
    onUpdated(() => {
      console.log('Component updated')
    })
    
    onUnmounted(() => {
      console.log('Component unmounted')
    })
    
    // 其他生命周期钩子
    // onBeforeMount
    // onBeforeUpdate
    // onBeforeUnmount
    // onErrorCaptured
    // onRenderTracked
    // onRenderTriggered
  }
}

5. 依赖注入

provide/inject

实现组件间的数据传递,不限于父子组件。

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

export default {
  setup() {
    const theme = ref('dark')
    
    provide('theme', theme)
    provide('changeTheme', (newTheme) => {
      theme.value = newTheme
    })
  }
}

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

export default {
  setup() {
    const theme = inject('theme', 'light') // 第二个参数是默认值
    const changeTheme = inject('changeTheme')
    
    return {
      theme,
      changeTheme
    }
  }
}

三、组合式函数(Composables)

1. 什么是组合式函数?

组合式函数是利用Composition API创建的可复用逻辑函数,通常以"use"开头命名。

2. 示例:创建一个useCounter组合式函数

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

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = initialValue
  }
  
  const doubleCount = computed(() => count.value * 2)
  
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}

使用组合式函数:

// components/Counter.vue
import { useCounter } from '../composables/useCounter'

export default {
  setup() {
    const { count, doubleCount, increment, decrement, reset } = useCounter(10)
    
    return {
      count,
      doubleCount,
      increment,
      decrement,
      reset
    }
  }
}

3. 实际案例:useApi

// composables/useApi.js
import { ref, reactive } from 'vue'

export function useApi(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetchData = async (params = {}) => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(`${url}?${new URLSearchParams(params)}`)
      
      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
    }
  }
  
  return {
    data,
    loading,
    error,
    fetchData
  }
}

使用useApi:

// components/UserList.vue
import { onMounted } from 'vue'
import { useApi } from '../composables/useApi'

export default {
  props: {
    page: {
      type: Number,
      default: 1
    }
  },
  
  setup(props) {
    const { data: users, loading, error, fetchData } = useApi('https://api.example.com/users')
    
    onMounted(() => {
      fetchData({ page: props.page })
    })
    
    return {
      users,
      loading,
      error,
      refresh: () => fetchData({ page: props.page })
    }
  }
}

四、Composition API实战技巧

1. 代码组织最佳实践

按功能组织相关的逻辑代码:

export default {
  setup() {
    // 用户相关逻辑
    const { user, loading: userLoading, fetchUser } = useUser()
    
    // 订单相关逻辑
    const { orders, loading: ordersLoading, fetchOrders } = useOrders()
    
    // 表单相关逻辑
    const { form, validate, resetForm } = useForm()
    
    // 生命周期
    onMounted(() => {
      fetchUser()
      fetchOrders()
    })
    
    return {
      // 用户相关
      user,
      userLoading,
      
      // 订单相关
      orders,
      ordersLoading,
      
      // 表单相关
      form,
      validate,
      resetForm
    }
  }
}

2. 与TypeScript结合使用

Composition API提供了更好的TypeScript支持:

import { defineComponent, ref, computed } from 'vue'

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

export default defineComponent({
  name: 'UserProfile',
  
  props: {
    userId: {
      type: Number,
      required: true
    }
  },
  
  setup(props) {
    const user = ref(null)
    const loading = ref(false)
    
    const isAdmin = computed(() => user.value?.role === 'admin')
    
    const fetchUser = async (): Promise => {
      loading.value = true
      try {
        // 类型安全的API调用
        const response = await fetch(`/api/users/${props.userId}`)
        user.value = await response.json()
      } catch (error) {
        console.error('Failed to fetch user:', error)
      } finally {
        loading.value = false
      }
    }
    
    return {
      user,
      loading,
      isAdmin,
      fetchUser
    }
  }
})

3. 异步组件与Suspense

使用Composition API创建异步组件:

// AsyncComponent.vue
import { defineComponent, ref, onMounted } from 'vue'

export default defineComponent({
  async setup() {
    const data = ref(null)
    
    // 模拟异步数据获取
    const fetchData = async () => {
      await new Promise(resolve => setTimeout(resolve, 1000))
      return { message: 'Hello from async component' }
    }
    
    // 直接返回Promise,配合Suspense使用
    data.value = await fetchData()
    
    return {
      data
    }
  }
})

// 父组件

4. 自定义指令

使用Composition API创建自定义指令:

// directives/vFocus.js
export default {
  mounted(el, binding) {
    if (binding.value) {
      el.focus()
    }
  },
  
  updated(el, binding) {
    if (binding.value && !binding.oldValue) {
      el.focus()
    }
  }
}

// 使用
import { defineComponent, ref } from 'vue'
import vFocus from './directives/vFocus'

export default defineComponent({
  directives: {
    focus: vFocus
  },
  
  setup() {
    const isEditing = ref(false)
    
    const startEditing = () => {
      isEditing.value = true
    }
    
    return {
      isEditing,
      startEditing
    }
  }
})

五、Composition API与Options API的选择

1. 什么时候使用Composition API?

  • 需要逻辑复用的复杂组件
  • 使用TypeScript开发
  • 组件逻辑复杂,需要按功能组织代码
  • 需要更好的树摇优化

2. 什么时候使用Options API?

  • 简单的组件
  • 团队更熟悉Options API
  • 迁移成本考虑

3. 混合使用

Vue 3支持在同一组件中混合使用两种API:

export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  
  setup() {
    const count = ref(0)
    
    function increment() {
      count.value++
    }
    
    return {
      count,
      increment
    }
  },
  
  methods: {
    greet() {
      console.log(this.message)
    }
  }
}

六、常见问题与解决方案

1. 响应性丢失问题

问题:解构reactive对象会失去响应性。

解决方案:使用toRefs或直接使用ref。

// 错误示例
const state = reactive({ count: 0 })
const { count } = state // 失去响应性

// 正确示例
const state = reactive({ count: 0 })
const { count } = toRefs(state) // 保持响应性

// 或者
const count = ref(0)

2. setup中的this

问题:在setup中this不是组件实例。

解决方案:setup接收props和context作为参数,不需要使用this。

setup(props, { emit }) {
  // 使用emit而不是this.$emit
  function handleClick() {
    emit('click', 'payload')
  }
}

3. 生命周期钩子的顺序

问题:不清楚Composition API和Options API生命周期的执行顺序。

执行顺序:

// 1. setup()
// 2. beforeCreate (Options API)
// 3. created (Options API)
// 4. onBeforeMount (Composition API)
// 5. beforeMount (Options API)
// 6. onMounted (Composition API)
// 7. mounted (Options API)

七、总结

Vue 3的Composition API为Vue.js带来了全新的编程范式,它通过函数式的方式组织组件逻辑,提供了更好的代码复用、类型推导和树摇优化。虽然学习曲线可能比Options API陡峭一些,但对于构建复杂的Vue应用来说,Composition API提供的灵活性和可维护性是值得的。

在实际项目中,我们可以根据具体需求选择合适的API风格,甚至混合使用。最重要的是,理解每种API的设计理念和适用场景,以便做出明智的选择。

随着Vue 3生态的不断完善,Composition API已经成为Vue开发的主流方式,掌握它将使我们能够更好地利用Vue.js的强大功能,构建高质量的前端应用。