为什么用组合式 API

Vue 2 的 Options API 把同一功能的逻辑分散在 data、methods、watch、computed 等选项里,组件一复杂,”一个功能的相关代码被拆得七零八落”的问题就很明显。Vue 3 的组合式 API(Composition API)允许我们按逻辑关注点组织代码,还能把可复用的逻辑抽成独立函数。

script setup 语法糖

从 Vue 3.2 开始,推荐使用 <script setup>,它让组合式 API 的写法更简洁:

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

// 顶层声明的变量、函数自动暴露给模板
const count = ref(0)

function increment() {
count.value++
}
</script>

<template>
<button @click="increment">count = {{ count }}</button>
</template>

使用 <script setup> 后不需要 export default,也不需要 return,组件自动变成”开箱即用”。

核心 API 速览

ref 与 reactive

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

// ref 用于基本类型,也可以包对象(访问时自动解包)
const name = ref('张三')
console.log(name.value) // 张三

// reactive 用于对象,成员访问无需 .value
const user = reactive({
name: '张三',
age: 25
})
user.age++ // 直接改,无需 .value

一个容易踩的坑:reactive 对解构后的属性会丢失响应性:

1
2
3
4
5
6
// 错误写法:解构后 age 不再是响应式的
const { age } = user

// 正确写法:用 toRefs 解构
import { toRefs } from 'vue'
const { age } = toRefs(user)

computed 计算属性

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

const price = ref(100)
const quantity = ref(2)

const total = computed(() => price.value * quantity.value)

// 计算属性默认是只读的,需要可写时传入 get/set
const totalEditable = computed({
get: () => price.value * quantity.value,
set: (val) => {
price.value = val / quantity.value
}
})

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
26
import { ref, watch, watchEffect } from 'vue'

const keyword = ref('')

// 侦听单个数据源
watch(keyword, (newVal, oldVal) => {
console.log('搜索词变化:', oldVal, '->', newVal)
})

// 侦听多个数据源
watch([keyword, page], ([newK, newP], [oldK, oldP]) => {
// ...
})

// 侦听对象中的某个属性,需要传 getter
watch(
() => user.age,
(val) => {
console.log('年龄变为', val)
}
)

// watchEffect:自动追踪依赖,依赖变化即执行
watchEffect(() => {
console.log('当前搜索词:', keyword.value)
})

注意:watch 是惰性的(依赖变化才触发),watchEffect 一进来就会执行一次。

生命周期

组合式 API 中生命周期钩子统一改为 onXxx 形式,且只能在 setup 顶层调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { onMounted, onUpdated, onUnmounted } from 'vue'

onMounted(() => {
// DOM 已挂载,适合发请求、初始化第三方库
})

onUpdated(() => {
// 组件更新后
})

onUnmounted(() => {
// 组件卸载,清理定时器、取消订阅
})

自定义组合函数:逻辑复用的核心

把可复用的逻辑抽成以 use 开头的函数,这是组合式 API 最大的价值。

下面封装一个”计数器”组合函数:

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

export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const double = computed(() => count.value * 2)

function increment() {
count.value++
}

function reset() {
count.value = initialValue
}

return { count, double, increment, reset }
}

组件中直接使用:

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

const { count, double, increment, reset } = useCounter(10)
</script>

<template>
<p>count: {{ count }},double: {{ double }}</p>
<button @click="increment">+1</button>
<button @click="reset">重置</button>
</template>

再比如常见的”请求数据”组合函数:

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

export function useRequest(fetcher) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)

async function run(...args) {
loading.value = true
error.value = null
try {
data.value = await fetcher(...args)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}

return { data, loading, error, run }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { useRequest } from './useRequest'

const { data, loading, error, run } = useRequest((id) =>
fetch(`/api/user/${id}`).then((res) => res.json())
)

run(1) // 页面加载时触发
</script>

<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">出错了:{{ error.message }}</div>
<div v-else>{{ data }}</div>
</template>

组合式 API 的几个注意点

  1. 只能在 <script setup> 或 setup() 顶层调用,不能在 if、for 里调用响应式 API;
  2. ref 在模板中自动解包,写 {{ count }} 而不是 {{ count.value }};在 JS 中必须写 .value;
  3. 组合函数内部如果创建了定时器、事件监听,记得在 onUnmounted 里清理,避免内存泄漏;
  4. 项目级复用逻辑放到 src/composables/ 目录统一管理。

总结

组合式 API 的核心价值是按逻辑组织代码 + 函数级复用。先用好 ref / reactive / computed / watch 这四件套,再养成分层抽取 useXxx 组合函数的习惯,组件会越写越清晰。