为什么选择组合式 API?
Vue 3 引入的组合式 API(Composition API)是 Vue 框架最重要的进化之一。相比传统的选项式 API,组合式 API 提供了更好的代码组织方式,特别是在处理复杂组件逻辑时。
在选项式 API 中,一个功能的逻辑被分散在 data、computed、methods、watch 等不同选项中。当组件变大时,维护变得困难。组合式 API 允许我们按功能组织代码,相关逻辑集中在一起。
核心响应式 API
ref —— 基本类型的响应式
ref 用于创建一个响应式的引用,通常用于基本类型(string、number、boolean)。它返回一个带有 .value 属性的包装对象。
import { ref } from 'vue'
// 创建响应式引用
const count = ref(0)
const message = ref('Hello Vue 3')
const isVisible = ref(true)
// 在 JavaScript 中通过 .value 访问
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// 在模板中自动解包,无需 .value
// <template>
// <p>{{ count }}</p> <!-- 直接使用,无需 .value -->
// </template>
reactive —— 对象类型的响应式
reactive 用于创建一个深层响应式的对象,适合处理复杂的数据结构。
import { reactive } from 'vue'
const user = reactive({
name: '博豪',
age: 25,
skills: ['JavaScript', 'Vue', 'Node.js'],
profile: {
github: 'https://github.com/bohao',
blog: 'taptapminigame.cloud'
}
})
// 直接修改属性,无需 .value
user.name = '陈博豪'
user.skills.push('TypeScript')
user.profile.github = 'https://github.com/new-url'
何时用 ref,何时用 reactive? 一般来说,基本类型用
ref,对象类型用reactive。但实际开发中,很多开发者统一使用ref,因为它在赋值时更直观(不会丢失响应性)。
computed —— 计算属性
computed 用于创建基于其他响应式状态的派生值,具有缓存特性。
import { ref, computed } from 'vue'
const firstName = ref('陈')
const lastName = ref('博豪')
// 只读计算属性
const fullName = computed(() => firstName.value + lastName.value)
// 可写计算属性
const fullNameWritable = computed({
get: () => firstName.value + lastName.value,
set: (val) => {
firstName.value = val.charAt(0)
lastName.value = val.slice(1)
}
})
// 带缓存:只有当 firstName 或 lastName 变化时才重新计算
console.log(fullName.value) // '陈博豪'
watch 与 watchEffect —— 侦听器
Vue 3 提供了两种侦听响应式数据变化的方式。
import { ref, watch, watchEffect } from 'vue'
const keyword = ref('')
const page = ref(1)
// watch: 明确指定要侦听的数据
watch(keyword, (newVal, oldVal) => {
console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
page.value = 1 // 关键词变化时重置页码
})
// watch 多个源
watch([keyword, page], ([newKeyword, newPage]) => {
fetchData(newKeyword, newPage)
})
// watchEffect: 自动追踪依赖
watchEffect(() => {
// 自动追踪内部用到的所有响应式数据
console.log(`当前搜索: ${keyword.value}, 第 ${page.value} 页`)
fetchData(keyword.value, page.value)
})
生命周期钩子
组合式 API 中的生命周期钩子以 on 为前缀,需要在 setup() 中调用。
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// <script setup> 中直接使用
onMounted(() => {
console.log('组件已挂载')
// 适合:发起 API 请求、操作 DOM、初始化第三方库
})
onUnmounted(() => {
console.log('组件已卸载')
// 适合:清理定时器、取消事件监听、断开 WebSocket
})
<script setup> 语法糖
<script setup> 是 Vue 3.2 引入的编译时语法糖,让组合式 API 的使用更加简洁。
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import UserCard from './UserCard.vue'
// 所有的顶层变量、函数、导入都会自动暴露给模板
const count = ref(0)
const doubled = computed(() => count.value * 2)
const router = useRouter()
function increment() {
count.value++
}
onMounted(() => {
console.log('组件已挂载')
})
</script>
<template>
<div>
<p>{{ count }} × 2 = {{ doubled }}</p>
<button @click="increment">+1</button>
<UserCard />
</div>
</template>
相比传统的 setup() 函数,<script setup> 的优势:
- 更少的样板代码,不需要 return
- 更好的 TypeScript 类型推断
- 更好的 IDE 支持和代码提示
- 更好的编译时性能优化
实战:构建一个可复用的搜索组件
下面通过一个完整的实例来展示组合式 API 的实际应用。我们将构建一个带防抖的搜索组件。
第一步:创建 useDebounce 组合式函数
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timer = null
watch(value, (newVal) => {
clearTimeout(timer)
timer = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
return debouncedValue
}
第二步:创建 useFetch 组合式函数
// composables/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 fetchData() {
loading.value = true
error.value = null
try {
const res = await fetch(url.value || url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
if (typeof url === 'object' && url.value !== undefined) {
watchEffect(fetchData)
} else {
fetchData()
}
return { data, error, loading, refetch: fetchData }
}
第三步:组装搜索组件
<script setup>
import { ref, computed } from 'vue'
import { useDebounce } from '../composables/useDebounce'
import { useFetch } from '../composables/useFetch'
const keyword = ref('')
const debouncedKeyword = useDebounce(keyword, 500)
const apiUrl = computed(
() => `/api/search?q=${encodeURIComponent(debouncedKeyword.value)}`
)
const { data: results, loading, error } = useFetch(apiUrl)
</script>
<template>
<div class="search-component">
<input v-model="keyword" placeholder="输入搜索关键词..." />
<div v-if="loading" class="loading">搜索中...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<ul v-else-if="results?.length">
<li v-for="item in results" :key="item.id">
{{ item.title }}
</li>
</ul>
<p v-else-if="debouncedKeyword">没有找到相关结果</p>
</div>
</template>
最佳实践总结
- 组合式函数命名以 use 开头 —— 如
useAuth、useFetch,这是社区约定 - 按功能组织代码 —— 将相关的状态和逻辑放在同一个组合式函数中
- 保持组合式函数的纯净 —— 避免副作用,让调用方决定何时触发
- 返回 ref 而不是 reactive —— 返回 ref 可以在解构时保持响应性
- 合理使用 toRefs —— 当需要解构 reactive 对象时,使用
toRefs保持响应性
import { toRefs } from 'vue'
function useUser() {
const state = reactive({ name: '', age: 0 })
// 解构时保持响应性
return { ...toRefs(state) }
}
const { name, age } = useUser() // name 和 age 都是 ref
结语
Vue 3 的组合式 API 不仅是一种新的语法,更是一种全新的代码组织思维。它让逻辑复用变得更加自然,让大型组件的维护变得更加轻松。建议在新项目中优先使用 <script setup> + 组合式 API 的方式来构建组件。
如果你正在从 Vue 2 迁移,不必急于全面重构,可以在新组件中使用组合式 API,逐步过渡。Vue 3 完全兼容选项式 API,两种风格可以在同一个项目中共存。