Vue3组合式API:代码组织的艺术
如果你写过大型Vue2项目,一定遇到过这样的困扰:
- 一个组件几千行,data、methods、computed分散各处
- 想复用一段逻辑,只能用mixin,然后命名冲突
- TypeScript支持不完善,类型推断总有问题
组合式API(Composition API)就是为了解决这些问题而生的。它让你按功能组织代码,而不是按选项组织。
选项式API vs 组合式API
选项式API(Options API)
export default {
data() {
return {
count: 0,
user: null
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
},
fetchUser() {
// ...
}
},
mounted() {
this.fetchUser()
}
}
问题:
- 相关代码分散在不同选项中
- 逻辑复用困难(mixin有命名冲突)
- TypeScript类型推断不好
组合式API(Composition API)
import { ref, computed, onMounted } from 'vue'
// 计数器逻辑
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
// 用户逻辑
const user = ref(null)
async function fetchUser() {
user.value = await fetch('/api/user').then(r => r.json())
}
onMounted(fetchUser)
优势:
- 相关代码放在一起
- 逻辑可以提取成独立函数
- 完美的TypeScript支持
setup函数
``是setup函数的语法糖:
<!-- 等价于 -->
export default {
setup() {
const count = ref(0)
return { count }
}
}
<!-- 语法糖 -->
const count = ref(0)
所有顶层绑定自动暴露给模板。
Props和Emits
// 定义props
const props = defineProps({
title: String
})
// 定义emits
const emit = defineEmits(['update'])
// 使用
console.log(props.title)
emit('update', data)
响应式转换
Vue3提供响应式语法糖(实验性):
// 启用后,ref可以省略.value
let count = $ref(0)
count++ // 自动处理
// 解构保持响应性
const { x, y } = $(useMouse())
组合式函数
组合式函数(Composables)是Vue3复用逻辑的核心方式。
基础示例
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
<!-- 使用 -->
import { useCounter } from './useCounter'
const { count, doubleCount, increment } = useCounter(10)
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button>+1</button>
实用组合式函数
useMouse - 追踪鼠标位置:
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 }
}
useFetch - 数据获取:
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function fetch() {
loading.value = true
error.value = null
try {
const response = await fetch(toValue(url))
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// 支持响应式URL
watchEffect(fetch)
return { data, error, loading, refetch: fetch }
}
import { useFetch } from './useFetch'
const { data, loading, error } = useFetch('/api/users')
<div>Loading...</div>
<div>Error: {{ error.message }}</div>
<div>
<ul>
<li>
{{ user.name }}
</li>
</ul>
</div>
useLocalStorage - 本地存储:
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored ? JSON.parse(stored) : defaultValue)
watch(
data,
(newVal) => {
if (newVal === null) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newVal))
}
},
{ deep: true }
)
return data
}
import { useLocalStorage } from './useLocalStorage'
const theme = useLocalStorage('theme', 'light')
<button>
Toggle Theme
</button>
provide/inject
跨层级组件通信:
<!-- 祖先组件 -->
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
provide('toggleTheme', () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
})
<!-- 后代组件(任意层级) -->
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
<div>
<button>Toggle</button>
</div>
响应式provide
// 使用Symbol避免命名冲突
export const ThemeKey = Symbol('theme')
// 提供默认值
const theme = inject(ThemeKey, 'light')
// 只读
provide(ThemeKey, readonly(theme))
模板引用
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
v-for中的ref
const listRef = ref([])
function setRef(el) {
if (el) {
listRef.value.push(el)
}
}
<div>
{{ item }}
</div>
组件ref
import ChildComponent from './Child.vue'
const childRef = ref(null)
function callChildMethod() {
childRef.value.someMethod()
}
异步组件
import { defineAsyncComponent } from 'vue'
// 异步加载
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
// 带加载状态
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
最佳实践
命名约定
// 组合式函数以use开头
useCounter()
useMouse()
useFetch()
// 私有函数以_开头或使用__
function _internalHelper() {}
返回值
// 返回响应式引用
export function useCounter() {
const count = ref(0)
return { count } // 不要返回 count.value
}
// 使用时保持响应性
const { count } = useCounter()
// count是ref,仍然是响应式的
清理副作用
export function useEventListener(target, event, callback) {
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
// 或者返回清理函数
return () => target.removeEventListener(event, callback)
}
// 使用
const cleanup = useEventListener(window, 'resize', handleResize)
cleanup() // 手动清理
参数处理
// 支持ref和原始值
import { toValue } from 'vue'
export function useFetch(url) {
// toValue会解包ref
const finalUrl = toValue(url)
watchEffect(() => {
fetch(toValue(url)) // 响应式URL
})
}
// 使用
const url = ref('/api/users')
const { data } = useFetch(url) // url变化时自动重新fetch
小结
你现在掌握了:
- 组合式API的核心思想(按功能组织代码)
- 组合式函数的编写和使用
- provide/inject跨层级通信
- 模板引用和异步组件
- 最佳实践和命名约定
下一期,我们进入Vue3生态,学习Vue Router路由和Pinia状态管理,完成一个实战项目。
练习任务:
- 编写一个
useCounter组合式函数 - 编写一个
useDebounce防抖函数 - 使用provide/inject实现主题切换
- 实现一个异步加载的组件
下期预告:《Vue3生态:Router、Pinia与实战项目》—— 路由怎么配?状态怎么管?来做一个完整项目!
Views: 1
