Vue 3 Composition API深度解析
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
}
}
})
// 父组件
Loading...
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的强大功能,构建高质量的前端应用。