Vue3组合式API的设计动机与核心概念
Vue3组合式API(Composition API)解决了Options API在复杂组件中逻辑分散的问题。在Vue2的Options API中,同一个功能的data、methods、computed分散在不同选项中,组件超过300行后维护成本急剧上升。组合式API允许将相关逻辑组织在一起,通过自定义Hook(Composables)实现逻辑复用。
组合式API的核心是ref和reactive两个响应式API。ref用于包装基本类型值,reactive用于包装对象。TypeScript实战中,ref通过泛型推断类型,reactive自动解包嵌套属性。Web性能优化的角度,组合式API的编译产物比Options API更小,Tree-shaking效果更好。
ref与reactive的选择策略与陷阱
ref和reactive的选择是Vue3开发中的高频问题。基本原则:基本类型用ref,对象/数组用reactive。但实际开发中有更多细节需要注意。
import { ref, reactive, computed, watch } from 'vue'
// ref:基本类型,需要.value访问
const count = ref(0)
count.value++
const userName = ref<string>('')
const userList = ref<User[]>([])
// reactive:对象类型,直接访问属性
const state = reactive({
loading: false,
data: [] as Product[],
error: null as Error | null
})
state.loading = true
// 常见陷阱:reactive解构后失去响应性
const { loading, data } = state // 失去响应性
// 正确做法:使用toRefs保持响应性
import { toRefs } from 'vue'
const { loading, data } = toRefs(state)
reactive的另一个陷阱是整体替换对象会丢失响应性。当从API获取数据后需要赋值整个对象时,应使用Object.assign或逐属性赋值,而非直接替换reactive变量:
// 错误:替换后不再是Proxy对象
state = { loading: false, data: newProducts, error: null }
// 正确方式一:Object.assign
Object.assign(state, { loading: false, data: newProducts, error: null })
// 正确方式二:用ref包装对象
const state = ref({ loading: false, data: [] })
state.value = { loading: false, data: newProducts }
自定义Hook封装模式与实战
自定义Hook是Vue3逻辑复用的标准模式。一个设计良好的Hook应该遵循单一职责原则,接收响应式输入,返回响应式输出,并正确处理生命周期清理。以下是一个完整的CRUD数据请求Hook:
// composables/useFetch.ts
import { ref, watch, isRef, unref, onUnmounted } from 'vue'
interface UseFetchOptions<T> {
immediate?: boolean
initialData?: T
onSuccess?: (data: T) => void
onError?: (error: Error) => void
}
export function useFetch<T>(
url: string | Ref<string>,
options: UseFetchOptions<T> = {}
) {
const { immediate = true, initialData } = options
const data = ref<T | undefined>(initialData)
const error = ref<Error | null>(null)
const loading = ref(false)
let abortController: AbortController | null = null
async function execute() {
loading.value = true
error.value = null
abortController?.abort()
abortController = new AbortController()
try {
const response = await fetch(unref(url), {
signal: abortController.signal,
headers: { 'Content-Type': 'application/json' }
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json() as T
options.onSuccess?.(data.value)
} catch (e) {
if (e.name !== 'AbortError') {
error.value = e as Error
options.onError?.(error.value)
}
} finally {
loading.value = false
}
}
if (isRef(url)) {
watch(url, execute)
}
onUnmounted(() => abortController?.abort())
if (immediate) execute()
return { data, error, loading, execute, refresh: execute }
}
使用Hook的组件代码极其简洁,逻辑复用的效果立竿见影:
// 组件中使用
const { data, error, loading, refresh } = useFetch<Product[]>('/api/products', {
onSuccess: (data) => console.log(`加载到 ${data.length} 条数据`)
})
响应式数据流管理与watch使用规范
复杂组件中响应式数据的流转通过computed和watch实现。computed用于派生状态,watch用于副作用。前端工程化中,watch的正确使用直接影响Web性能优化效果。
import { ref, computed, watch, watchEffect } from 'vue'
import { debounce } from 'lodash-es'
const searchQuery = ref('')
const page = ref(1)
const pageSize = ref(20)
const allItems = ref<Item[]>([])
// computed:派生状态,自动缓存
const filteredItems = computed(() => {
if (!searchQuery.value) return allItems.value
return allItems.value.filter(item => item.name.includes(searchQuery.value))
})
const pagedItems = computed(() => {
const start = (page.value - 1) * pageSize.value
return filteredItems.value.slice(start, start + pageSize.value)
})
const totalPages = computed(() =>
Math.ceil(filteredItems.value.length / pageSize.value)
)
// watch:副作用,带防抖
const debouncedSearch = debounce((q: string) => {
page.value = 1
console.log('执行搜索:', q)
}, 300)
watch(searchQuery, (newVal) => debouncedSearch(newVal))
// watchEffect:自动收集依赖
watchEffect(() => {
document.title = `列表 - 第${page.value}页 / 共${totalPages.value}页`
})
// watch多个数据源
watch([page, pageSize], ([newPage, newSize], [oldPage, oldSize]) => {
if (newPage !== oldPage || newSize !== oldSize) {
console.log(`分页变化: ${oldPage},${oldSize} -> ${newPage},${newSize}`)
}
}, { flush: 'post' })
watch的flush选项影响执行时机:默认’pre’在组件更新前执行,’post’在DOM更新后执行(适合需要访问更新后DOM的场景),’sync’同步执行(性能影响大,不推荐)。
组件库设计中的组合式API实践
响应式布局和组件库设计中,组合式API的灵活性更加突出。以可拖拽面板组件为例,将拖拽逻辑封装为useDraggable Hook,与面板UI组件解耦:
// composables/useDraggable.ts
export function useDraggable(target: Ref<HTMLElement | null>) {
const isDragging = ref(false)
const position = reactive({ x: 0, y: 0 })
const offset = reactive({ x: 0, y: 0 })
function onMouseDown(e: MouseEvent) {
isDragging.value = true
offset.x = e.clientX - position.x
offset.y = e.clientY - position.y
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
function onMouseMove(e: MouseEvent) {
if (!isDragging.value) return
position.x = e.clientX - offset.x
position.y = e.clientY - offset.y
}
function onMouseUp() {
isDragging.value = false
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
onUnmounted(() => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
})
return { isDragging, position, onMouseDown }
}
Vue3组合式API通过自定义Hook模式,将组件逻辑从模板和样式中剥离,实现了高内聚低耦合的代码组织。TypeScript类型推导在组合式API中表现更好,ref和reactive的响应式系统相比Vue2的defineProperty方案,对数组和Map/Set的支持更完善,避免了Vue2中$set、$delete等特殊API的使用。在实际跨端小程序开发中,组合式API同样适用,逻辑层Hook可直接复用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shen-du-shi-zhan-zi-ding-yi-hook-feng/