组合式 API(Composition API)是 Vue 3 最重要的特性,解决了 Options API 在复杂组件中逻辑分散的问题。本文通过实战案例深入讲解 setup、响应式 API、生命周期钩子和自定义组合函数。
一、为什么需要组合式 API
Options API 的痛点
// Options API:同一功能的代码被拆散到不同选项中
export default {
data() {
return {
// 搜索功能的数据
searchQuery: '',
searchResults: [],
// 用户功能的数据
user: null,
userPermissions: [],
}
},
computed: {
// 搜索功能的计算属性
filteredResults() { /* ... */ },
// 用户功能的计算属性
isAdmin() { /* ... */ },
},
methods: {
// 搜索功能的方法
handleSearch() { /* ... */ },
// 用户功能的方法
loadUser() { /* ... */ },
},
mounted() {
// 搜索和用户的初始化逻辑混在一起
this.handleSearch()
this.loadUser()
}
}
组合式 API 的优势
<script setup>
// 搜索功能的所有代码聚合在一起
const { searchQuery, searchResults, handleSearch } = useSearch()
// 用户功能的所有代码聚合在一起
const { user, isAdmin, loadUser } = useUser()
</script>
| 优势 | 说明 |
|---|
| 逻辑复用 | 自定义组合函数替代 mixin,无命名冲突 |
| 代码组织 | 相关逻辑聚合,而非按选项类型分散 |
| 类型推导 | 更好的 TypeScript 支持 |
| 代码压缩 | setup 语法糖减少样板代码 |
| 生产性能 | 编译优化(静态提升、patch flag) |
二、setup 与 script setup
基本用法
<script setup>
import { ref, computed, onMounted } from 'vue'
// 顶层变量自动暴露给模板
const count = ref(0)
const message = 'Hello'
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('组件已挂载')
})
</script>
<template>
<button @click="increment">{{ count }} × 2 = {{ double }}</button>
</template>
defineProps 与 defineEmits
<script setup>
// 编译宏,无需导入
const props = defineProps({
title: String,
items: {
type: Array,
default: () => []
},
disabled: Boolean
})
// TypeScript 方式
// const props = defineProps<{
// title: string
// items: Item[]
// disabled?: boolean
// }>()
const emit = defineEmits(['update', 'delete'])
// 带验证的 emit
// const emit = defineEmits<{
// (e: 'update', value: number): void
// (e: 'delete', id: string): void
// }>()
function handleUpdate() {
emit('update', props.title)
}
</script>
defineExpose 与 defineModel
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 暴露给父组件的方法
defineExpose({
reset: () => { count.value = 0 },
getValue: () => count.value
})
// Vue 3.4+ defineModel 双向绑定
const modelValue = defineModel({ default: '' })
const visible = defineModel('visible', { type: Boolean, default: false })
</script>
三、响应式 API
ref 与 reactive
import { ref, reactive, shallowRef, shallowReactive } from 'vue'
// ref — 适合基本类型和需要整体替换的对象
const count = ref(0)
const user = ref({ name: '张三', age: 25 })
console.log(count.value) // 访问需 .value
user.value.name = '李四' // 修改属性
user.value = { name: '王五' } // 整体替换
// reactive — 适合不替换整体的复合对象
const state = reactive({
items: [],
loading: false,
filters: { category: null, price: null }
})
state.loading = true // 直接访问,无需 .value
state.items.push({ id: 1 })
// ⚠️ reactive 的陷阱
let { items } = state // ❌ 解构丢失响应式
items = toRef(state, 'items') // ✅ 用 toRef 保持响应式
ref vs reactive 选择原则
| 场景 | 推荐 |
|---|
| 基本类型 | ref |
| 需要整体替换 | ref |
| 表单对象 | reactive |
| 组合函数返回值 | ref(保持统一) |
| 大型只读配置 | shallowRef / markRaw |
readonly 与 shallowRef
import { readonly, shallowRef, triggerRef, markRaw } from 'vue'
// readonly — 防止修改(如传递给子组件的 props)
const original = reactive({ count: 0 })
const copy = readonly(original)
// copy.count++ // ⚠️ 警告:无法修改
// shallowRef — 只追踪 .value 本身,适合大列表
const list = shallowRef([])
// 替换整个数组触发更新
list.value = [...list.value, newItem]
// 修改内部属性后手动触发
list.value.push(anotherItem)
triggerRef(list)
// markRaw — 排除响应式转换(如第三方类实例)
const chart = markRaw(new ECharts())
computed 与 watch
import { ref, computed, watch, watchEffect } from 'vue'
const firstName = ref('三')
const lastName = ref('张')
// computed — 有缓存
const fullName = computed(() => `${lastName.value}${firstName.value}`)
// 可写 computed
const fullNameWritable = computed({
get: () => `${lastName.value}${firstName.value}`,
set: (val) => {
lastName.value = val[0]
firstName.value = val.slice(1)
}
})
// watch — 明确监听源,可获取旧值
watch(fullName, (newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`)
})
// watch 对象属性
watch(
() => state.filters,
(newFilters) => { fetchList(newFilters) },
{ deep: true, immediate: true }
)
// watchEffect — 立即执行,自动追踪依赖
watchEffect(() => {
console.log(`${firstName.value} ${lastName.value}`)
})
// watchEffect 清理副作用
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log(firstName.value)
}, 500)
onCleanup(() => clearTimeout(timer)) // 依赖变化时清理
})
toRefs 与 toValue
import { toRefs, toRef, toValue, reactive } from 'vue'
const state = reactive({ x: 1, y: 2 })
// toRefs — 用于解构 reactive 对象时保持响应式
const { x, y } = toRefs(state)
// toRef — 单个属性
const yRef = toRef(state, 'y')
// toValue — 统一处理 ref / getter / 普通值(Vue 3.3+)
function resolveValue(maybeRefOrGetter) {
return toValue(maybeRefOrGetter)
}
resolveValue(refValue) // ref → .value
resolveValue(() => 123) // getter → 调用结果
resolveValue(123) // 普通值 → 原样返回
四、生命周期钩子
<script setup>
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onErrorCaptured, onActivated, onDeactivated
} from 'vue'
onBeforeMount(() => console.log('挂载前'))
onMounted(() => console.log('DOM 已就绪'))
onBeforeUpdate(() => console.log('更新前'))
onUpdated(() => console.log('更新后'))
onBeforeUnmount(() => console.log('卸载前'))
onUnmounted(() => console.log('已卸载'))
// 捕获子组件错误
onErrorCaptured((err, instance, info) => {
console.error('子组件错误:', err)
return false // 阻止继续传播
})
// KeepAlive 相关
onActivated(() => console.log('组件激活'))
onDeactivated(() => console.log('组件失活'))
</script>
五、自定义组合函数
useFetch — 数据请求
// composables/useFetch.js
import { ref, shallowRef, triggerRef, toValue, watchEffect } from 'vue'
export function useFetch(url) {
const data = shallowRef(null)
const error = ref(null)
const loading = ref(false)
async function doFetch() {
data.value = null
error.value = null
loading.value = true
try {
const res = await fetch(toValue(url))
if (!res.ok) throw new Error(res.statusText)
data.value = await res.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// url 是响应式时自动重新请求
watchEffect(doFetch)
return { data, error, loading, retry: doFetch }
}
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'
const userId = ref(1)
const { data: user, error, loading } = useFetch(
() => `/api/users/${userId.value}` // getter 形式
)
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<div v-else-if="user">{{ user.name }}</div>
</template>
useLocalStorage — 本地存储
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(
JSON.parse(localStorage.getItem(key)) ?? defaultValue
)
watch(value, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
function remove() {
localStorage.removeItem(key)
value.value = defaultValue
}
return { value, remove }
}
useMouse — 鼠标追踪
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
useDebounceFn — 防抖
// composables/useDebounceFn.js
import { customRef } from 'vue'
export function debouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
六、provide 与 inject
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'
const theme = ref('dark')
const user = ref({ name: '张三' })
// 提供只读数据
provide('theme', readonly(theme))
provide('user', readonly(user))
// 提供修改方法
provide('updateTheme', (val) => { theme.value = val })
</script>
<!-- 后代组件(任意深度) -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 带默认值
const user = inject('user')
const updateTheme = inject('updateTheme')
updateTheme('light')
</script>
七、小结
组合式 API 是 Vue 3 的核心范式:ref/reactive 管理状态,computed/watch 处理派生与副作用,自定义组合函数实现逻辑复用。掌握这些 API 后,你会发现复杂组件的开发变得清晰而高效。
本文由 inspirecl.asia 原创,转载请注明出处。
评论 (0)