Vue 3 组合式 API 进阶
1. 深入响应式
1.1 ref vs reactive 内部原理
javascript
// ref 内部实现 (简化)
class RefImpl {
constructor(value) {
this._value = value
this._dep = new Dep()
}
get value() {
this._dep.depend() // 收集依赖
return this._value
}
set value(newVal) {
this._value = newVal
this._dep.notify() // 触发更新
}
}
// reactive 内部实现 (简化)
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const dep = getDep(target, key)
dep.depend()
const value = Reflect.get(target, key)
// 深层响应式
if (typeof value === 'object' && value !== null) {
return reactive(value)
}
return value
},
set(target, key, value) {
const dep = getDep(target, key)
target[key] = value
dep.notify()
return true
}
})
}1.2 shallowRef 和 triggerRef
vue
<script setup>
import { shallowRef, triggerRef, ref } from 'vue'
// shallowRef: 只有 .value 变化才触发更新
const shallow = shallowRef({ count: 0 })
shallow.value.count++ // 不会触发更新!
shallow.value = { count: 1 } // 才会触发
// triggerRef: 手动触发 shallowRef 更新
const state = shallowRef({ items: [] })
function updateItems() {
// 直接修改内部属性不会触发
state.value.items.push({ id: 1 })
// 需要手动触发
triggerRef(state)
}
// ref: 深层响应式
const deep = ref({ items: [] })
deep.value.items.push({ id: 1 }) // 会自动触发
</script>1.3 readonly 和 shallowReadonly
javascript
import { readonly, shallowReadonly, ref, reactive } from 'vue'
const count = ref(0)
const obj = reactive({ count: 0 })
// readonly: 深层只读
const readOnlyCount = readonly(count)
readOnlyCount.value = 1 // 警告: Cannot set readonly property
// shallowReadonly: 只有第一层只读
const shallowReadOnly = shallowReadonly({
count: 0,
nested: { value: 1 }
})
shallowReadOnly.nested.value = 2 // 可以修改!
shallowReadOnly.count = 1 // 警告
// 应用场景
const state = reactive({
user: { name: '张三' },
settings: { theme: 'dark' }
})
// 暴露给外部前设置为只读
function useStore() {
return {
// 只读的用户信息
user: readonly(state.user),
// 可修改的设置
settings: state.settings
}
}1.4 toRaw 和 markRaw
javascript
import { toRaw, markRaw, isRef, isProxy } from 'vue'
const obj = reactive({ name: '张三' })
// toRaw: 获取原始对象
const raw = toRaw(obj)
console.log(obj === raw) // true
// 应用: 性能优化,避免重复代理
function processData(data) {
const raw = toRaw(data)
// 直接操作原始对象,不触发响应式
raw.processed = true
return raw
}
// markRaw: 标记为非响应式
const nonReactive = markRaw({
name: '张三',
methods: {
greet() { console.log('Hello') }
}
})
// 放入 reactive 中不会变成响应式
const state = reactive({
items: [],
meta: nonReactive // meta 不会响应式
})
state.meta.greet() // 可以调用
state.items.push(nonReactive) // 可以,但 reactive 检测不到变化
// 注意事项
state.meta.name = '李四' // 修改不会触发响应式更新2. 高级组合式函数
2.1 useAsync
javascript
// composables/useAsync.js
import { ref, shallowRef } from 'vue'
export function useAsync(asyncFn, options = {}) {
const { immediate = false, onSuccess, onError } = options
const data = shallowRef(null)
const error = shallowRef(null)
const isLoading = ref(false)
const isSuccess = ref(false)
const isError = ref(false)
async function execute(...args) {
isLoading.value = true
error.value = null
isSuccess.value = false
isError.value = false
try {
const result = await asyncFn(...args)
data.value = result
isSuccess.value = true
onSuccess?.(result)
return result
} catch (e) {
error.value = e
isError.value = true
onError?.(e)
throw e
} finally {
isLoading.value = false
}
}
if (immediate) {
execute()
}
return {
data,
error,
isLoading,
isSuccess,
isError,
execute
}
}
// 使用
const { data: users, isLoading, error, execute } = useAsync(
(id) => fetch(`/api/users/${id}`).then(r => r.json()),
{ immediate: true }
)
// 手动触发
const { execute } = useAsync(
(payload) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(payload)
}).then(r => r.json())
)
async function createUser() {
try {
await execute({ name: '张三', age: 25 })
console.log('创建成功')
} catch (e) {
console.error('创建失败:', e)
}
}2.2 useDebounce
javascript
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timeout
watch(value, (newVal) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
return debouncedValue
}
// 完整版
export function useDebounceFn(fn, delay = 300) {
let timeout
function debounced(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
fn(...args)
}, delay)
}
function cancel() {
clearTimeout(timeout)
}
function flush(...args) {
cancel()
fn(...args)
}
return { debounced, cancel, flush }
}
// 使用
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)
watch(debouncedQuery, (query) => {
// 搜索
searchAPI(query)
})2.3 useLocalStorage
javascript
// composables/useLocalStorage.js
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 || newVal === undefined) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newVal))
}
}, { deep: true })
return data
}
// 使用
const theme = useLocalStorage('theme', 'light')
const userPrefs = useLocalStorage('prefs', { sidebar: true, fontSize: 14 })2.4 useMediaQuery
javascript
// composables/useMediaQuery.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMediaQuery(query) {
const matches = ref(false)
let mediaQuery
function update() {
matches.value = mediaQuery.matches
}
onMounted(() => {
mediaQuery = window.matchMedia(query)
update()
mediaQuery.addEventListener('change', update)
})
onUnmounted(() => {
mediaQuery?.removeEventListener('change', update)
})
return matches
}
// 使用
const isMobile = useMediaQuery('(max-width: 768px)')
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
const isLandscape = useMediaQuery('(orientation: landscape)')3. 组件进阶
3.1 异步组件
vue
<!-- AsyncComponent.vue -->
<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorBoundary from './ErrorBoundary.vue'
// 基础异步组件
const AsyncUser = defineAsyncComponent(() =>
import('./User.vue')
)
// 带选项的异步组件
const AsyncUserWithOptions = defineAsyncComponent({
loader: () => import('./User.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorBoundary,
delay: 200,
timeout: 3000,
onError(error, retry, fail, attempts) {
// 错误处理
if (attempts < 3) {
retry() // 重试
} else {
fail() // 放弃
}
}
})
</script>
<template>
<Suspense>
<template #default>
<AsyncUser />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>3.2 依赖注入进阶
javascript
// provide key
const INJECTION_KEY = Symbol('myPlugin')
const MY_STRING_KEY = 'myString'
export function providePlugin(app) {
app.provide(INJECTION_KEY, {
version: '1.0.0',
config: { apiUrl: 'https://api.example.com' }
})
}
// 组件中
import { inject } from 'vue'
export default {
setup() {
// 使用 Symbol key
const plugin = inject(INJECTION_KEY)
// 使用字符串 key + 默认值
const myString = inject(MY_STRING_KEY, 'default value')
// 注入响应式数据
const { state, update } = inject('sharedState')
return { plugin, myString, state, update }
}
}3.3 渲染函数
vue
<script setup>
import { h, ref } from 'vue'
const count = ref(0)
// 方式1: 使用 render 函数
const vnode = h('div', { class: 'container' }, [
h('h1', '计数器: ' + count.value),
h('button', { onClick: () => count.value++ }, '+1')
])
// 方式2: 结合模板
// 需要在 setup 中返回 render 函数
function render() {
return h('div', { class: 'wrapper' }, [
h('p', `计数: ${count.value}`),
h('button', { onClick: () => count.value-- }, '-1')
])
}
export default { render }
</script>4. 性能优化
4.1 v-memo
vue
<template>
<!-- 只有 title 或 deep === 3 时才重新渲染 -->
<div v-memo="[title, deep === 3]">
<h2>{{ title }}</h2>
<p>{{ content }}</p>
<span>{{ timestamp }}</span>
</div>
</template>
<script setup>
const title = ref('标题')
const content = ref('内容')
const timestamp = ref(Date.now())
const deep = ref(3)
</script>4.2 v-once
vue
<template>
<!-- 只渲染一次,不会改变 -->
<div v-once>
<StaticContent />
</div>
<!-- 结合 v-if -->
<div v-if="show">
<ExpensiveComponent v-once />
</div>
</template>4.3 路由懒加载优化
javascript
// 路由配置
const routes = [
// 方式1: 直接 import
{ path: '/', component: import('./Home.vue') },
// 方式2: 带注释控制 chunk 名称
{
path: '/about',
component: () => import(/* webpackChunkName: "about" */ './About.vue')
},
// 方式3: 预加载
{
path: '/user',
component: () => import(/* webpackPrefetch: true */ './User.vue')
}
]
// 组件中预加载
import { preloadRouteComponents } from 'vue-router/auto'
// 用户悬停时预加载
function onHoverLink(to) {
preloadRouteComponents(to).catch(() => {})
}4.4 虚拟列表
vue
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
items: Array,
itemHeight: { type: Number, default: 50 },
visibleCount: { type: Number, default: 10 }
})
const containerRef = ref(null)
const scrollTop = ref(0)
const totalHeight = computed(() => props.items.length * props.itemHeight)
const visibleData = computed(() => {
const start = Math.floor(scrollTop.value / props.itemHeight)
const end = start + props.visibleCount + 2
return props.items.slice(start, end).map((item, index) => ({
...item,
top: (start + index) * props.itemHeight
}))
})
function onScroll(e) {
scrollTop.value = e.target.scrollTop
}
</script>
<template>
<div
ref="containerRef"
class="virtual-list"
@scroll="onScroll"
>
<div :style="{ height: totalHeight + 'px' }">
<div
v-for="item in visibleData"
:key="item.id"
class="virtual-item"
:style="{ transform: `translateY(${item.top}px)` }"
>
{{ item.name }}
</div>
</div>
</div>
</template>5. TypeScript 支持
5.1 组件类型定义
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { PropType } from 'vue'
interface User {
id: number
name: string
email: string
}
interface Props {
title: string
users: User[]
type: 'info' | 'warning' | 'error'
onClick?: (user: User) => void
}
const props = withDefaults(defineProps<Props>(), {
title: '默认标题',
users: () => [],
type: 'info'
})
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
// ref 类型推导
const count = ref(0) // Ref<number>
const name = ref('') // Ref<string>
// 手动类型
const count2 = ref<number | null>(null)
// computed 类型推导
const doubleCount = computed(() => count.value * 2)
// 函数类型
const handleClick = (user: User) => {
emit('click', user)
}
</script>5.2 组合式函数类型定义
typescript
// composables/useUser.ts
import { ref, computed, type Ref } from 'vue'
interface User {
id: number
name: string
}
interface UseUserReturn {
user: Ref<User | null>
isLoading: Ref<boolean>
error: Ref<Error | null>
fetchUser: (id: number) => Promise<void>
updateUser: (data: Partial<User>) => Promise<void>
}
export function useUser(): UseUserReturn {
const user = ref<User | null>(null)
const isLoading = ref(false)
const error = ref<Error | null>(null)
async function fetchUser(id: number) {
isLoading.value = true
error.value = null
try {
const res = await fetch(`/api/users/${id}`)
user.value = await res.json()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
async function updateUser(data: Partial<User>) {
if (!user.value) return
// ...
}
return {
user,
isLoading,
error,
fetchUser,
updateUser
}
}5.3 模板引用类型
vue
<script setup lang="ts">
import { ref, type ComponentPublicInstance } from 'vue'
// 获取子组件实例类型
interface ChildComponentExposed {
open: (id: string) => void
close: () => void
value: string
}
const childRef = ref<ComponentPublicInstance<ChildComponentExposed> | null>(null)
function handleClick() {
if (childRef.value) {
childRef.value.open('123')
}
}
// 获取原生元素类型
const inputRef = ref<HTMLInputElement | null>(null)
function focusInput() {
inputRef.value?.focus()
}
// 获取组件实例
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)
</script>
<template>
<MyComponent ref="childRef" />
<input ref="inputRef" />
</template>6. 面试要点
6.1 响应式原理
Vue 3 使用 Proxy 实现响应式:
ref:
- 基本类型: 包装成 { value: xxx }
- 对象类型: reactive(value)
reactive:
- 直接返回 Proxy
- 深层代理,所有嵌套属性都响应式
对比 Vue 2:
- Vue 2 使用 Object.defineProperty
- 无法监听新增属性 (需要 $set)
- 无法监听数组索引变化
- 需要递归遍历,性能差6.2 组合式函数设计原则
1. 单一职责
- 每个 composable 只做一件事
- useUser, useAuth, usePermission 分开
2. 显式依赖
- 参数明确,副作用可控
3. 返回一致
- 统一返回 { state, actions, ... }
4. 便于测试
- composable 逻辑可以单独测试
5. 命名规范
- useXxx 开头
- 返回的 ref 命名清晰6.3 性能优化技巧
1. 路由懒加载
- component: () => import('./view.vue')
2. v-memo
- 大量列表渲染时,控制不必要的更新
3. v-once
- 不变的静态内容
4. shallowRef
- 大数据结构,不需要深层响应式
5. localStorage 封装
- 减少持久化开销
6. 虚拟列表
- 长列表必备6.4 nextTick 原理
javascript
// 简单实现
class Scheduler {
callbacks = []
pending = false
flush() {
this.pending = false
const cbs = [...this.callbacks]
this.callbacks = []
cbs.forEach(cb => cb())
}
nextTick(cb) {
return new Promise(resolve => {
this.callbacks.push(() => {
cb()
resolve()
})
if (!this.pending) {
this.pending = true
Promise.resolve().then(() => this.flush())
}
})
}
}
// 使用
await nextTick()
// DOM 已经更新