Vue3 组合式 API 完全指南

Vue3 组合式 API 完全指南

Vue3组合式API

Vue3 的组合式 API 为我们提供了一种更灵活、更强大的组件逻辑组织方式。本文将全面介绍组合式 API 的使用方法、最佳实践以及与选项式 API 的对比。

一、为什么需要组合式 API?

1.1 选项式 API 的局限性

在 Vue2 中,我们使用选项式 API 来组织组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
export default {
data() {
return {
count: 0,
user: null
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
},
async fetchUser() {
this.user = await fetchUser()
}
},
mounted() {
this.fetchUser()
}
}
</script>

存在的问题:

  • 逻辑分散:相关逻辑被分散在 data、methods、computed 等选项中
  • 代码复用困难:需要通过 Mixins 或高阶组件来实现
  • TypeScript 支持有限:类型推断不够精确
  • this 指向问题:箭头函数无法使用 this

1.2 组合式 API 的优势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const user = ref(null)

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

function increment() {
count.value++
}

async function fetchUser() {
user.value = await fetchUser()
}

onMounted(fetchUser)
</script>

优势:

  • 逻辑集中:相关代码可以组织在一起
  • 更好的复用:通过组合函数实现逻辑复用
  • 完美的 TypeScript 支持
  • 没有 this 指向问题

二、核心 API 详解

2.1 ref 和 reactive

ref

ref 用于创建响应式的原始值或对象:

1
2
3
4
5
6
7
8
9
import { ref } from 'vue'

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

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

ref 的使用场景:

  • 原始值(string, number, boolean)
  • 需要整体替换的对象
  • 在模板中直接使用(会自动解包)

reactive

reactive 用于创建响应式的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { reactive } from 'vue'

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

// 访问和修改
console.log(state.count) // 0
state.count = 1
state.user.name = 'Bob'

reactive 的使用场景:

  • 深层嵌套的对象
  • 需要保持对象引用的场景

2.2 computed

computed 用于创建计算属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref, computed } from 'vue'

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

// 可写的计算属性
const count = ref(0)
const doubleCount = computed({
get: () => count.value * 2,
set: (val) => {
count.value = val / 2
}
})

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

computed 的特点:

  • 基于响应式依赖缓存
  • 只有依赖改变时才会重新计算
  • 可以设置 getter 和 setter

2.3 watch 和 watchEffect

watch

watch 用于观察特定的数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})

// 观察多个源
watch([count, message], ([newCount, newMessage]) => {
console.log('Both changed')
})

// 使用选项对象
watch(
() => state.user,
(newUser) => {
console.log('User changed', newUser)
},
{
deep: true,
immediate: true,
flush: 'post'
}
)

watchEffect

watchEffect 会自动追踪依赖:

1
2
3
4
5
6
7
8
9
10
import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
console.log(`Count is: ${count.value}`)
})

// 自动追踪 count.value,不需要显式指定
count.value = 1 // 会触发 watchEffect

两者的区别:

  • watch:明确指定要观察的数据源
  • watchEffect:自动追踪函数内的响应式依赖

2.4 生命周期钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount
} from 'vue'

onMounted(() => {
console.log('Component mounted')
})

onUpdated(() => {
console.log('Component updated')
})

onUnmounted(() => {
console.log('Component unmounted')
})

三、组合函数(Composables)

3.1 什么是组合函数?

组合函数是利用 Vue 组合式 API 来封装和复用有状态逻辑的函数。

3.2 基础示例:鼠标位置追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
const x = ref(0)
const y = ref(0)

function update(event) {
x.value = event.pageX
y.value = event.pageY
}

onMounted(() => {
window.addEventListener('mousemove', update)
})

onUnmounted(() => {
window.removeEventListener('mousemove', update)
})

return { x, y }
}

使用组合函数:

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { useMouse } from './useMouse'

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

<template>
<div>
Mouse position: {{ x }}, {{ y }}
</div>
</template>

3.3 进阶示例:数据请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// useFetch.js
import { ref, watchEffect } from 'vue'

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

async function doFetch() {
data.value = null
error.value = null
loading.value = true

try {
const response = await fetch(url.value)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}

watchEffect(doFetch)

return { data, error, loading }
}

3.4 接受参数的组合函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// useCounter.js
import { ref, computed } from 'vue'

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

const increment = () => {
count.value += step
}

const decrement = () => {
count.value -= step
}

const reset = () => {
count.value = initialValue
}

const double = computed(() => count.value * 2)

return {
count,
increment,
decrement,
reset,
double
}
}

四、最佳实践

4.1 逻辑提取

将可复用的逻辑提取为独立的组合函数:

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 好的做法
import { useMouse } from './composables/useMouse'
import { useScroll } from './composables/useScroll'

const { x, y } = useMouse()
const { scrollTop } = useScroll()

// ❌ 不好的做法:所有逻辑都写在组件中
const mousePos = { x: 0, y: 0 }
const scrollPos = 0

// 大量的事件监听代码...

4.2 命名规范

组合函数通常以 “use” 开头:

1
2
3
4
5
6
7
8
9
10
// ✅ 好的命名
useMouse()
useFetch()
useCounter()
useLocalStorage()

// ❌ 不好的命名
mouse()
fetchData()
counter()

4.3 TypeScript 支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// useMouse.ts
import { ref, Ref } from 'vue'

interface MousePosition {
x: Ref<number>
y: Ref<number>
}

export function useMouse(): MousePosition {
const x = ref(0)
const y = ref(0)

// ... 实现

return { x, y }
}

// 使用时具有完整的类型提示
const { x, y } = useMouse()
// x.value 类型为 number

五、与选项式 API 的混合使用

5.1 在选项式 API 中使用组合式 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
import { ref } from 'vue'

export default {
data() {
return {
optionData: '来自选项式 API'
}
},
setup() {
const compositionData = ref('来自组合式 API')

return {
compositionData
}
},
methods: {
handleClick() {
console.log(this.optionData)
console.log(this.compositionData)
}
}
}
</script>

5.2 迁移策略

  1. 渐进式迁移:新组件使用组合式 API,旧组件保持不变
  2. 提取逻辑:将可复用逻辑提取为组合函数
  3. 逐步重构:一次迁移一个组件

六、常见陷阱与解决方案

6.1 ref 解包问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 错误:在 reactive 中解包 ref
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0,不是 Ref 对象

// ✅ 正确:使用 toRefs
import { toRefs } from 'vue'
const state = reactive({
count: ref(0),
message: ref('Hello')
})
const { count, message } = toRefs(state)

6.2 响应式丢失

1
2
3
4
5
6
7
8
9
// ❌ 错误:解构会失去响应性
const state = reactive({ count: 0, message: 'Hello' })
const { count, message } = state
count++ // 不会触发更新

// ✅ 正确:使用 toRefs
import { toRefs } from 'vue'
const { count, message } = toRefs(state)
count.value++ // 会触发更新

6.3 深层响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// reactive 会递归转换
const state = reactive({
user: {
profile: {
name: 'Alice'
}
}
})

state.user.profile.name = 'Bob' // 会触发更新

// ref 默认不会深层转换
const state = ref({
user: {
profile: {
name: 'Alice'
}
}
})

state.value.user.profile.name = 'Bob' // 不会触发更新

// ✅ 使用 shallowRef 或深层响应
import { shallowRef, triggerRef } from 'vue'
const state = shallowRef({ count: 0 })
state.value = { count: 1 } // 会触发更新
state.value.count = 2 // 不会触发更新

七、性能优化

7.1 使用 shallowRef 和 shallowReactive

1
2
3
4
5
6
7
import { shallowRef, shallowReactive } from 'vue'

// 对于大型对象,使用 shallow 避免深层响应式
const largeObject = shallowRef({ /* 大量数据 */ })

// 手动触发更新
largeObject.value = { ...largeObject.value, updated: true }

7.2 合理使用 computed

1
2
3
4
5
6
7
// ✅ 好的做法:使用 computed 缓存
const expensiveValue = computed(() => {
return heavyComputation(state.value)
})

// ❌ 不好的做法:在模板中直接计算
<div>{{ heavyComputation(state) }}</div>

八、总结

Vue3 的组合式 API 为我们提供了更强大、更灵活的代码组织方式。通过组合函数,我们可以更好地复用逻辑、维护代码。

关键要点:

  1. 使用 ref 处理原始值,reactive 处理对象
  2. 通过组合函数实现逻辑复用
  3. 使用 toRefs 避免响应式丢失
  4. 合理使用 computed 和 watch
  5. 注意性能优化,避免不必要的响应式

掌握组合式 API 是成为 Vue3 高级开发者的必经之路。