Vue 3 高级使用指南:从入门到精通的实战之路
嘿,朋友们!今天咱们聊聊Vue 3的那些高级玩法。说实话,Vue 3刚出来的时候,我也是一脸懵逼,什么Composition API、Proxy响应式系统...听起来就很唬人。但是用了一段时间后,我发现这些"高级"功能其实都是为了让我们的开发体验更爽、代码更优雅。
🚀 引言:为什么要学Vue 3的高级用法?
你可能会问:"我用Options API写得好好的,为什么要学这些复杂的东西?"
哈哈,这个问题我当初也问过自己。直到有一天,我需要在多个组件之间复用一套复杂的状态逻辑,然后我就体验到了什么叫"代码地狱"。组件越来越臃肿,逻辑越来越混乱,维护起来简直要命。
这时候Vue 3的Composition API就像救星一样出现了。它不仅让代码组织更合理,还提供了更强大的类型推断、更好的逻辑复用能力。所以说,掌握Vue 3的高级用法,不是为了装X,而是为了让自己的代码更优雅、更易维护。
🎯 Chapter 1: Composition API - 不只是语法糖
1.1 告别Options API的烦恼
还记得Options API的痛点吗?当组件逻辑复杂起来,你得在data、methods、computed、watch之间来回跳转,简直像在玩"找不同"游戏。
javascript
// 这是我们以前写的代码,看起来是不是很分散?
export default {
data() {
return {
user: null,
loading: false,
error: null,
posts: [],
currentPage: 1
}
},
computed: {
totalPages() {
return Math.ceil(this.posts.length / 10)
}
},
methods: {
async fetchUser() {
this.loading = true
try {
this.user = await api.getUser()
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
},
async fetchPosts() {
// 又是一堆异步逻辑...
}
},
watch: {
currentPage() {
this.fetchPosts()
}
}
}
1.2 Composition API的优雅解决方案
现在我们用Composition API重写,看看有什么不同:
javascript
import { ref, computed, watch, onMounted } from 'vue'
export default {
setup() {
// 用户相关逻辑集中在一起
const user = ref(null)
const userLoading = ref(false)
const userError = ref(null)
const fetchUser = async () => {
userLoading.value = true
try {
user.value = await api.getUser()
} catch (err) {
userError.value = err.message
} finally {
userLoading.value = false
}
}
// 文章相关逻辑也集中在一起
const posts = ref([])
const currentPage = ref(1)
const postsLoading = ref(false)
const totalPages = computed(() => Math.ceil(posts.value.length / 10))
const fetchPosts = async () => {
postsLoading.value = true
try {
posts.value = await api.getPosts(currentPage.value)
} catch (err) {
console.error('Failed to fetch posts:', err)
} finally {
postsLoading.value = false
}
}
watch(currentPage, fetchPosts)
onMounted(() => {
fetchUser()
fetchPosts()
})
return {
user, userLoading, userError,
posts, currentPage, totalPages, postsLoading,
fetchUser, fetchPosts
}
}
}
看到区别了吗?相关的逻辑现在都聚集在一起,而不是被Options API的结构强行分散。这就是Composition API的魅力 - 按逻辑关注点组织代码,而不是按API类型。
1.3 响应式基础 - ref vs reactive
这是每个Vue 3开发者都会遇到的经典问题:"什么时候用ref,什么时候用reactive?"
我的经验法则是这样的:
javascript
import { ref, reactive, toRefs } from 'vue'
// 基本类型用ref
const count = ref(0)
const message = ref('hello')
const isVisible = ref(true)
// 对象用reactive(但要小心解构)
const state = reactive({
user: { name: 'John', age: 30 },
settings: { theme: 'dark', lang: 'zh' },
cache: new Map()
})
// 错误的解构方式 - 会失去响应性!
const { user, settings } = state // ❌ 别这样做
// 正确的解构方式
const { user, settings } = toRefs(state) // ✅ 这样才对
// 或者你可以全用ref,这样更安全
const user = ref({ name: 'John', age: 30 })
const settings = ref({ theme: 'dark', lang: 'zh' })
我个人倾向于尽量使用ref,因为它的行为更可预测,不容易因为解构而丢失响应性。
1.4 计算属性的高级用法
计算属性不只是简单的getter,它还有很多高级玩法:
javascript
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 基础用法
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 可写计算属性
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value) {
[firstName.value, lastName.value] = value.split(' ')
}
})
// 带缓存的复杂计算
const expensiveValue = computed(() => {
console.log('这个计算很昂贵,只有依赖变化时才会重新计算')
return heavyCalculation(someReactiveData.value)
})
// 条件计算属性
const conditionalValue = computed(() => {
if (!user.value) return null
return user.value.permissions.includes('admin') ? 'Administrator' : 'User'
})
1.5 侦听器的强大功能
Vue 3的watch比你想象的更强大:
javascript
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const user = ref(null)
// 基础侦听
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// 侦听多个值
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
console.log('count or user changed')
})
// 深度侦听对象
const userProfile = ref({ name: 'John', settings: { theme: 'dark' } })
watch(userProfile, (newVal) => {
console.log('User profile changed')
}, { deep: true })
// 立即执行的侦听
watch(user, (newUser) => {
if (newUser) {
fetchUserPosts(newUser.id)
}
}, { immediate: true })
// watchEffect - 自动收集依赖
watchEffect(() => {
// 这个函数会立即执行,并且当count或user变化时重新执行
if (count.value > 10 && user.value) {
sendNotification()
}
})
// 异步侦听器
watchEffect(async (onInvalidate) => {
const controller = new AbortController()
onInvalidate(() => {
controller.abort() // 清理副作用
})
try {
const data = await fetch('/api/data', {
signal: controller.signal
})
// 处理数据
} catch (error) {
if (!controller.signal.aborted) {
console.error('Fetch failed:', error)
}
}
})
🔄 Chapter 2: 响应式系统深度解析
2.1 从Object.defineProperty到Proxy的飞跃
Vue 2的响应式系统基于Object.defineProperty
,虽然好用,但有一些限制:
javascript
// Vue 2的烦恼
const obj = { a: 1 }
// 新增属性不响应
obj.b = 2 // ❌ 不会触发更新
// 数组的某些操作不响应
const arr = [1, 2, 3]
arr[0] = 999 // ❌ 不会触发更新
arr.length = 0 // ❌ 不会触发更新
Vue 3用Proxy重写了响应式系统,彻底解决了这些问题:
javascript
import { reactive } from 'vue'
const obj = reactive({ a: 1 })
obj.b = 2 // ✅ 完全响应式
const arr = reactive([1, 2, 3])
arr[0] = 999 // ✅ 响应式
arr.length = 0 // ✅ 响应式
arr.push(4) // ✅ 响应式
2.2 响应式系统的内部机制
让我们看看Vue 3响应式系统的工作原理:
javascript
// 简化版的响应式实现(帮助理解原理)
let activeEffect = null
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (deps) {
deps.forEach(effect => effect())
}
}
function reactive(target) {
return new Proxy(target, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
return true
}
})
}
2.3 shallowRef和shallowReactive的使用场景
有时候我们不需要深层响应式,这时候shallow版本就派上用场了:
javascript
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef - 只有.value是响应式的
const shallowUser = shallowRef({
name: 'John',
profile: { age: 30, city: 'New York' }
})
// 这会触发更新
shallowUser.value = { name: 'Jane', profile: { age: 25, city: 'LA' } }
// 这不会触发更新
shallowUser.value.name = 'Bob'
shallowUser.value.profile.age = 35
// 如果你修改了深层属性,需要手动触发更新
shallowUser.value.name = 'Bob'
triggerRef(shallowUser) // 手动触发更新
// shallowReactive - 只有第一层是响应式的
const shallowState = shallowReactive({
count: 0, // 响应式
user: {
name: 'John', // 非响应式
age: 30 // 非响应式
}
})
什么时候用shallow版本呢?
- 大型数据结构:当你有一个巨大的对象,但只关心顶层属性时
- 第三方库对象:当你需要让第三方库的对象具有响应性,但不想Vue深度代理时
- 性能优化:当深度响应性造成性能问题时
🧩 Chapter 3: 自定义Composables - 逻辑复用的艺术
3.1 什么是Composables?
Composables是Vue 3中复用逻辑的主要方式,你可以把它想象成"带状态的工具函数"。
让我们从一个简单的例子开始:
javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
const isEven = computed(() => count.value % 2 === 0)
const isPositive = computed(() => count.value > 0)
return {
count,
increment,
decrement,
reset,
isEven,
isPositive
}
}
使用起来非常简单:
javascript
// 在组件中使用
import { useCounter } from '@/composables/useCounter'
export default {
setup() {
const { count, increment, decrement, isEven } = useCounter(10)
return {
count,
increment,
decrement,
isEven
}
}
}
3.2 实战案例:useLocalStorage
让我们写一个更实用的composable:
javascript
// composables/useLocalStorage.js
import { ref, watch, Ref } from 'vue'
export function useLocalStorage(key, defaultValue, options = {}) {
const {
serializer = JSON,
onError = (e) => console.error(e)
} = options
const read = () => {
try {
const item = localStorage.getItem(key)
if (item === null) return defaultValue
return serializer.parse(item)
} catch (error) {
onError(error)
return defaultValue
}
}
const write = (value) => {
try {
if (value === null || value === undefined) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, serializer.stringify(value))
}
} catch (error) {
onError(error)
}
}
const storedValue = read()
const state = ref(storedValue)
// 监听状态变化,自动保存到localStorage
watch(state, write, { deep: true })
// 监听localStorage的变化(其他tab页面的修改)
window.addEventListener('storage', (e) => {
if (e.key === key && e.newValue !== null) {
try {
state.value = serializer.parse(e.newValue)
} catch (error) {
onError(error)
}
}
})
return state
}
使用这个composable:
javascript
// 在组件中使用
const userPreferences = useLocalStorage('user-preferences', {
theme: 'light',
language: 'en',
notifications: true
})
// 修改会自动保存到localStorage
userPreferences.value.theme = 'dark'
3.3 异步数据获取:useFetch
这是一个非常实用的composable,处理API请求的各种状态:
javascript
// composables/useFetch.js
import { ref, watch, computed } from 'vue'
export function useFetch(url, options = {}) {
const {
immediate = true,
beforeFetch = (ctx) => ctx,
afterFetch = (ctx) => ctx,
onError = () => {}
} = options
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
const isFinished = ref(false)
const abortController = ref()
const abort = () => {
if (abortController.value) {
abortController.value.abort()
abortController.value = null
}
}
const execute = async (executeUrl = url) => {
abort()
isLoading.value = true
isFinished.value = false
error.value = null
abortController.value = new AbortController()
try {
const context = {
url: executeUrl,
options: {
signal: abortController.value.signal,
...options
}
}
const { url: fetchUrl, options: fetchOptions } = beforeFetch(context)
const response = await fetch(fetchUrl, fetchOptions)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const responseData = await response.json()
const afterFetchContext = { data: responseData, response }
const { data: processedData } = afterFetch(afterFetchContext)
data.value = processedData
} catch (fetchError) {
if (fetchError.name !== 'AbortError') {
error.value = fetchError
onError(fetchError)
}
} finally {
isLoading.value = false
isFinished.value = true
abortController.value = null
}
}
// 计算属性
const canAbort = computed(() => isLoading.value)
if (immediate && url) {
execute()
}
return {
data,
error,
isLoading,
isFinished,
canAbort,
execute,
abort
}
}
高级用法:
javascript
// 带缓存的版本
export function useFetchWithCache(url, options = {}) {
const cache = new Map()
return useFetch(url, {
...options,
beforeFetch(ctx) {
const cached = cache.get(ctx.url)
if (cached && Date.now() - cached.timestamp < 60000) { // 1分钟缓存
return Promise.resolve(cached.data)
}
return ctx
},
afterFetch(ctx) {
cache.set(url, {
data: ctx.data,
timestamp: Date.now()
})
return ctx
}
})
}
3.4 表单处理:useForm
表单处理是前端开发的常见任务,让我们写一个强大的表单composable:
javascript
// composables/useForm.js
import { reactive, computed } from 'vue'
export function useForm(initialValues = {}, validationRules = {}) {
const form = reactive({ ...initialValues })
const errors = reactive({})
const touched = reactive({})
// 验证单个字段
const validateField = (field, value) => {
const rules = validationRules[field]
if (!rules) return true
for (const rule of rules) {
const result = rule(value)
if (result !== true) {
errors[field] = result
return false
}
}
delete errors[field]
return true
}
// 验证所有字段
const validate = () => {
let isValid = true
for (const field in form) {
if (!validateField(field, form[field])) {
isValid = false
}
touched[field] = true
}
return isValid
}
// 设置字段值
const setFieldValue = (field, value) => {
form[field] = value
if (touched[field]) {
validateField(field, value)
}
}
// 设置字段为已触摸
const setFieldTouched = (field, isTouched = true) => {
touched[field] = isTouched
if (isTouched) {
validateField(field, form[field])
}
}
// 重置表单
const reset = () => {
Object.assign(form, initialValues)
Object.keys(errors).forEach(key => delete errors[key])
Object.keys(touched).forEach(key => delete touched[key])
}
// 计算属性
const isValid = computed(() => Object.keys(errors).length === 0)
const isDirty = computed(() => {
return Object.keys(form).some(key => form[key] !== initialValues[key])
})
return {
form,
errors,
touched,
isValid,
isDirty,
validate,
setFieldValue,
setFieldTouched,
reset
}
}
// 常用验证规则
export const validators = {
required: (message = 'This field is required') => {
return (value) => {
if (!value || value.toString().trim() === '') {
return message
}
return true
}
},
email: (message = 'Invalid email address') => {
return (value) => {
if (!value) return true
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(value) || message
}
},
minLength: (min, message) => {
return (value) => {
if (!value) return true
return value.length >= min || message || `Minimum ${min} characters required`
}
},
maxLength: (max, message) => {
return (value) => {
if (!value) return true
return value.length <= max || message || `Maximum ${max} characters allowed`
}
}
}
使用示例:
javascript
import { useForm, validators } from '@/composables/useForm'
export default {
setup() {
const { form, errors, isValid, validate, setFieldValue, setFieldTouched } = useForm(
{
name: '',
email: '',
password: ''
},
{
name: [validators.required('Name is required')],
email: [
validators.required('Email is required'),
validators.email('Please enter a valid email')
],
password: [
validators.required('Password is required'),
validators.minLength(8, 'Password must be at least 8 characters')
]
}
)
const handleSubmit = () => {
if (validate()) {
console.log('Form is valid:', form)
// 提交表单
}
}
return {
form,
errors,
isValid,
handleSubmit,
setFieldValue,
setFieldTouched
}
}
}
🎨 Chapter 4: 高级组件模式
4.1 Renderless组件模式
Renderless组件是一种强大的模式,它只提供逻辑,而把渲染交给使用者。这种模式在Vue 3中非常流行:
javascript
// components/ClickOutside.vue
<template>
<slot :isOutside="isOutside" />
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
export default {
name: 'ClickOutside',
setup(props, { slots }) {
const isOutside = ref(false)
const handleClickOutside = (event) => {
// 检查点击是否在组件外部
const slotElement = slots.default?.()?.[0]?.el
if (slotElement && !slotElement.contains(event.target)) {
isOutside.value = true
setTimeout(() => {
isOutside.value = false
}, 100)
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
return {
isOutside
}
}
}
</script>
使用这个renderless组件:
vue
<template>
<ClickOutside v-slot="{ isOutside }">
<div class="dropdown" :class="{ 'clicked-outside': isOutside }">
<button @click="toggleDropdown">Toggle</button>
<div v-if="isOpen" class="dropdown-menu">
<p>Dropdown content</p>
</div>
</div>
</ClickOutside>
</template>
4.2 高阶组件(HOC)模式
Vue 3中可以用函数式的方式创建高阶组件:
javascript
// hoc/withLoading.js
import { h, defineComponent } from 'vue'
export function withLoading(WrappedComponent, loadingComponent) {
return defineComponent({
name: `WithLoading(${WrappedComponent.name})`,
props: {
isLoading: {
type: Boolean,
default: false
}
},
setup(props, { attrs, slots }) {
return () => {
if (props.isLoading) {
return loadingComponent ? h(loadingComponent) : h('div', 'Loading...')
}
return h(WrappedComponent, attrs, slots)
}
}
})
}
使用高阶组件:
javascript
import { withLoading } from '@/hoc/withLoading'
import UserProfile from '@/components/UserProfile.vue'
import LoadingSpinner from '@/components/LoadingSpinner.vue'
const UserProfileWithLoading = withLoading(UserProfile, LoadingSpinner)
export default {
components: {
UserProfileWithLoading
},
setup() {
const { data: user, isLoading } = useFetch('/api/user')
return {
user,
isLoading
}
}
}
4.3 动态组件和异步组件
Vue 3在动态组件方面有很多强大的功能:
javascript
// 动态组件的高级用法
import { defineAsyncComponent, ref, computed } from 'vue'
// 异步组件定义
const AsyncUserProfile = defineAsyncComponent({
loader: () => import('@/components/UserProfile.vue'),
loadingComponent: () => h('div', 'Loading user profile...'),
errorComponent: () => h('div', 'Failed to load user profile'),
delay: 200,
timeout: 3000
})
// 条件异步加载
const getComponentByType = (type) => {
const componentMap = {
user: () => import('@/components/UserCard.vue'),
product: () => import('@/components/ProductCard.vue'),
article: () => import('@/components/ArticleCard.vue')
}
return defineAsyncComponent(componentMap[type])
}
export default {
setup() {
const currentView = ref('user')
// 动态计算当前组件
const currentComponent = computed(() => {
return getComponentByType(currentView.value)
})
return {
currentView,
currentComponent
}
}
}
4.4 Provide/Inject高级模式
Provide/Inject是Vue 3中非常强大的依赖注入机制:
javascript
// providers/ThemeProvider.js
import { provide, inject, ref, computed } from 'vue'
const ThemeSymbol = Symbol('theme')
export function provideTheme() {
const currentTheme = ref('light')
const themes = {
light: {
primary: '#007bff',
background: '#ffffff',
text: '#333333'
},
dark: {
primary: '#4dabf7',
background: '#1a1a1a',
text: '#ffffff'
}
}
const themeConfig = computed(() => themes[currentTheme.value])
const toggleTheme = () => {
currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'
}
const themeContext = {
currentTheme,
themeConfig,
toggleTheme
}
provide(ThemeSymbol, themeContext)
return themeContext
}
export function useTheme() {
const themeContext = inject(ThemeSymbol)
if (!themeContext) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return themeContext
}
在根组件中提供:
vue
<!-- App.vue -->
<template>
<div class="app" :style="{
backgroundColor: themeConfig.background,
color: themeConfig.text
}">
<router-view />
</div>
</template>
<script>
import { provideTheme } from '@/providers/ThemeProvider'
export default {
setup() {
const { themeConfig } = provideTheme()
return {
themeConfig
}
}
}
</script>
在子组件中使用:
vue
<!-- components/ThemeToggle.vue -->
<template>
<button
@click="toggleTheme"
:style="{ backgroundColor: themeConfig.primary }"
>
Switch to {{ currentTheme === 'light' ? 'dark' : 'light' }} theme
</button>
</template>
<script>
import { useTheme } from '@/providers/ThemeProvider'
export default {
setup() {
const { currentTheme, themeConfig, toggleTheme } = useTheme()
return {
currentTheme,
themeConfig,
toggleTheme
}
}
}
</script>