vue3一些不常用卻很實(shí)用API匯總
前言
最近在重構(gòu)一個(gè)老移動(dòng)端項(xiàng)目,用Vue3重寫的過(guò)程中,發(fā)現(xiàn)了一些平時(shí)不太注意的API,但用起來(lái)真的很香。今天分享幾個(gè)我覺(jué)得特別實(shí)用的,希望能幫到正在學(xué)習(xí)Vue3的jym。
1. shallowRef 和 shallowReactive - ‘大數(shù)據(jù)’性能提升
先說(shuō)個(gè)場(chǎng)景:你有一個(gè)很大的對(duì)象,里面嵌套了很多層數(shù)據(jù),但你只需要最外層的變化觸發(fā)更新。這時(shí)候用 ref 或 reactive 就會(huì)把所有嵌套都變成響應(yīng)式的,性能開(kāi)銷很大。
import { ref, shallowRef, reactive, shallowReactive, triggerRef } from 'vue'
// 普通 ref - 深度響應(yīng)式
const deepData = ref({
user: {
profile: {
name: '張三',
address: {
city: '北京',
// ... 還有很多層
}
}
}
})
// shallowRef - 淺層響應(yīng)式,只監(jiān)聽(tīng)第一層
const shallowData = shallowRef({
user: {
profile: {
name: '李四'
}
}
})
// 只有直接替換整個(gè)對(duì)象才會(huì)觸發(fā)更新
shallowData.value = { user: { profile: { name: '王五' } } } // ? 會(huì)觸發(fā)更新
// 修改內(nèi)部屬性不會(huì)觸發(fā)更新
shallowData.value.user.profile.name = '趙六' // ? 不會(huì)觸發(fā)更新
// 但可以用 triggerRef 手動(dòng)觸發(fā)更新
shallowData.value.user.profile.name = '趙六'
triggerRef(shallowData) // ? 手動(dòng)觸發(fā)更新,視圖會(huì)刷新
// shallowReactive - 淺層響應(yīng)式的 reactive 版本
const shallowState = shallowReactive({
user: {
name: '張三',
age: 20
}
})
shallowState.user = { name: '李四', age: 25 } // ? 會(huì)觸發(fā)更新
shallowState.user.name = '王五' // ? 不會(huì)觸發(fā)更新
我在處理表格數(shù)據(jù)的時(shí)候經(jīng)常用這個(gè),特別是那種幾千行的數(shù)據(jù),用 shallowRef 能明顯感覺(jué)到性能提升。shallowReactive 和 shallowRef 的區(qū)別在于,前者直接返回響應(yīng)式對(duì)象,后者返回的是包裝后的 ref。
triggerRef 的使用場(chǎng)景:當(dāng)你使用 shallowRef 時(shí),如果確實(shí)需要修改嵌套屬性并觸發(fā)更新,但又不想替換整個(gè)對(duì)象(可能很耗性能),就可以先修改屬性,然后調(diào)用 triggerRef 手動(dòng)觸發(fā)更新。這樣既享受了淺層響應(yīng)式的性能優(yōu)勢(shì),又能在需要時(shí)精確控制更新時(shí)機(jī)。
// 實(shí)際應(yīng)用:批量修改后統(tǒng)一觸發(fā)更新
const tableData = shallowRef([
{ id: 1, name: '張三', status: 'active' },
{ id: 2, name: '李四', status: 'active' },
// ... 幾千條數(shù)據(jù)
])
// 批量修改多個(gè)嵌套屬性
tableData.value.forEach(item => {
item.status = 'inactive' // 修改嵌套屬性
item.updatedAt = Date.now() // 繼續(xù)修改
})
// 所有修改完成后,統(tǒng)一觸發(fā)一次更新(性能更好)
triggerRef(tableData) // ? 只觸發(fā)一次更新,而不是每條數(shù)據(jù)都觸發(fā)
2. markRaw - 讓某些數(shù)據(jù)"免疫"響應(yīng)式
有些數(shù)據(jù)你明確知道不需要響應(yīng)式,比如第三方庫(kù)的實(shí)例、DOM元素、或者一些配置對(duì)象。用 markRaw 標(biāo)記后,Vue就不會(huì)把它變成響應(yīng)式的。
import { reactive, markRaw } from 'vue'
const state = reactive({
// 這個(gè)第三方庫(kù)實(shí)例不需要響應(yīng)式
chart: markRaw(new ChartLibrary()),
// 這個(gè)配置對(duì)象也不需要
config: markRaw({
apiUrl: 'https://api.example.com',
timeout: 5000
}),
// 這個(gè)需要響應(yīng)式
count: 0
})
// 即使把 chart 放到 reactive 里,它也不會(huì)變成響應(yīng)式
// 這樣既避免了不必要的性能開(kāi)銷,也防止了某些庫(kù)因?yàn)轫憫?yīng)式代理而出錯(cuò)
我之前用 ECharts 的時(shí)候就遇到過(guò)這個(gè)問(wèn)題,圖表實(shí)例被響應(yīng)式代理后各種報(bào)錯(cuò),用 markRaw 一包就解決了。
3. toRaw - 獲取原始對(duì)象
有時(shí)候你需要拿到響應(yīng)式對(duì)象的原始值,比如傳給第三方庫(kù)、或者做深拷貝。toRaw 就是干這個(gè)的。
import { reactive, toRaw } from 'vue'
const state = reactive({
name: 'Vue3',
version: '3.0'
})
// 獲取原始對(duì)象,不再是 Proxy
const rawState = toRaw(state)
// 傳給需要普通對(duì)象的函數(shù)
someThirdPartyLibrary.process(rawState)
// 或者做深拷貝
const copy = JSON.parse(JSON.stringify(toRaw(state)))
注意,toRaw 只能拿到一層,如果對(duì)象里還有嵌套的響應(yīng)式對(duì)象,那些還是 Proxy。需要遞歸處理的話,可以配合 markRaw 使用。
4. customRef - 自定義響應(yīng)式引用
這個(gè)API特別適合做防抖、節(jié)流,或者需要自定義 get/set 邏輯的場(chǎng)景。
import { customRef } from 'vue'
// 防抖的 ref
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track() // 追蹤依賴
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 觸發(fā)更新
}, delay)
}
}
})
}
// 使用
const searchText = useDebouncedRef('')
// 用戶輸入時(shí)不會(huì)立即更新,等停止輸入 200ms 后才更新
我在搜索框里經(jīng)常用這個(gè),避免用戶每輸入一個(gè)字符就發(fā)一次請(qǐng)求。
5. readonly - 創(chuàng)建只讀的響應(yīng)式對(duì)象
有時(shí)候你想讓某些數(shù)據(jù)是響應(yīng)式的(能追蹤變化),但不允許修改。readonly 就派上用場(chǎng)了。
import { reactive, readonly } from 'vue'
const original = reactive({
count: 0,
name: 'Vue3'
})
// 創(chuàng)建只讀版本
const readOnlyData = readonly(original)
// 可以讀取
console.log(readOnlyData.count) // 0
// 不能修改(開(kāi)發(fā)環(huán)境會(huì)警告)
readOnlyData.count = 1 // ?? 警告:Set operation on key "count" failed
// 但修改原始對(duì)象,只讀版本也會(huì)更新
original.count = 1
console.log(readOnlyData.count) // 1 ?
這個(gè)在組件間傳遞 props 的時(shí)候很有用,特別是你想確保子組件不會(huì)意外修改父組件的數(shù)據(jù)。
6. getCurrentInstance - 獲取當(dāng)前組件實(shí)例
這個(gè)API在組合式API里獲取組件實(shí)例特別方便,雖然官方文檔說(shuō)主要給庫(kù)開(kāi)發(fā)者用,但實(shí)際開(kāi)發(fā)中偶爾也會(huì)用到。
import { getCurrentInstance } from 'vue'
export default {
setup() {
const instance = getCurrentInstance()
// 獲取組件的 props
console.log(instance.props)
// 獲取組件的 attrs
console.log(instance.attrs)
// 獲取組件的 emit
instance.emit('custom-event')
// 訪問(wèn)全局屬性
const $router = instance.appContext.config.globalProperties.$router
return {}
}
}
不過(guò)要注意,getCurrentInstance 只能在 setup 或生命周期鉤子里調(diào)用,而且不能緩存到異步回調(diào)里用。
7. defineAsyncComponent - 異步組件加載
這個(gè)其實(shí)不算特別冷門,但很多人可能沒(méi)深入用過(guò)。除了基本的懶加載,它還有很多高級(jí)用法。
import { defineAsyncComponent } from 'vue'
// 基礎(chǔ)用法
const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))
// 帶加載狀態(tài)和錯(cuò)誤處理
const AsyncComponentWithStates = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner, // 加載時(shí)顯示的組件
errorComponent: ErrorComponent, // 錯(cuò)誤時(shí)顯示的組件
delay: 200, // 延遲顯示 loading,避免閃爍
timeout: 3000, // 超時(shí)時(shí)間
onError(error, retry, fail, attempts) {
// 錯(cuò)誤處理邏輯
if (attempts <= 3) {
retry() // 重試
} else {
fail() // 放棄
}
}
})
我在做路由懶加載和按需加載大組件的時(shí)候,這個(gè)API幫了不少忙。
8. watchEffect 的立即執(zhí)行和清理
watchEffect 大家都知道,但它的立即執(zhí)行特性和清理函數(shù)可能很多人沒(méi)充分利用。
import { watchEffect, ref } from 'vue'
const count = ref(0)
const stop = watchEffect((onInvalidate) => {
// 這個(gè)函數(shù)會(huì)立即執(zhí)行一次
console.log('count:', count.value)
// 清理函數(shù):在下次執(zhí)行前或停止監(jiān)聽(tīng)時(shí)調(diào)用
onInvalidate(() => {
console.log('清理之前的副作用')
// 比如取消請(qǐng)求、清除定時(shí)器等
})
})
// 手動(dòng)停止監(jiān)聽(tīng)
stop()
這個(gè)特性在處理副作用的時(shí)候特別有用,比如監(jiān)聽(tīng)數(shù)據(jù)變化自動(dòng)發(fā)送請(qǐng)求,可以在清理函數(shù)里取消之前的請(qǐng)求,避免競(jìng)態(tài)條件。
import { watchEffect, ref } from 'vue'
const userId = ref(1)
let abortController = null
watchEffect((onInvalidate) => {
// 取消之前的請(qǐng)求
if (abortController) {
abortController.abort()
}
// 創(chuàng)建新的請(qǐng)求
abortController = new AbortController()
fetch(`/api/user/${userId.value}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => {
// 處理數(shù)據(jù)
})
// 清理函數(shù):在下次執(zhí)行前或組件卸載時(shí)調(diào)用
onInvalidate(() => {
if (abortController) {
abortController.abort()
}
})
})
這樣就能避免快速切換用戶ID時(shí),舊的請(qǐng)求覆蓋新請(qǐng)求結(jié)果的問(wèn)題。
9. unref、isRef、isReactive - 類型檢查工具
這些工具函數(shù)在處理可能是響應(yīng)式也可能不是的數(shù)據(jù)時(shí)特別有用。
import { ref, reactive, unref, isRef, isReactive, isReadonly } from 'vue'
// unref - 如果是 ref 就返回 .value,否則返回原值
function useValue(maybeRef) {
return unref(maybeRef) // 不用判斷是不是 ref,直接 unref 就行
}
const count = ref(10)
const name = 'Vue3'
console.log(useValue(count)) // 10
console.log(useValue(name)) // 'Vue3'
// isRef - 判斷是否是 ref
if (isRef(count)) {
console.log('這是一個(gè) ref')
}
// isReactive - 判斷是否是 reactive 對(duì)象
const state = reactive({ count: 0 })
if (isReactive(state)) {
console.log('這是一個(gè) reactive 對(duì)象')
}
// isReadonly - 判斷是否是 readonly 對(duì)象
const readonlyState = readonly(state)
if (isReadonly(readonlyState)) {
console.log('這是一個(gè) readonly 對(duì)象')
}
在寫工具函數(shù)或者組合式函數(shù)的時(shí)候,這些檢查函數(shù)能讓你寫出更健壯的代碼。
10. effectScope 和 onScopeDispose - 批量管理副作用
effectScope 是 Vue3.2 新增的,可以批量管理一組響應(yīng)式效果(computed、watch、watchEffect等),統(tǒng)一停止或清理。
import { effectScope, ref, watchEffect, computed, onScopeDispose } from 'vue'
function useMouse() {
const x = ref(0)
const y = ref(0)
// 創(chuàng)建一個(gè)作用域
const scope = effectScope()
scope.run(() => {
// 在這個(gè)作用域內(nèi)的所有響應(yīng)式效果都會(huì)被統(tǒng)一管理
watchEffect(() => {
const handler = (e) => {
x.value = e.clientX
y.value = e.clientY
}
window.addEventListener('mousemove', handler)
// 清理函數(shù)
onScopeDispose(() => {
window.removeEventListener('mousemove', handler)
})
})
// 也可以有多個(gè) watchEffect、computed 等
const distance = computed(() => {
return Math.sqrt(x.value ** 2 + y.value ** 2)
})
})
// 返回停止函數(shù)
return {
x,
y,
stop: () => scope.stop() // 一次性停止所有效果
}
}
這個(gè)在寫組合式函數(shù)的時(shí)候特別有用,可以確保所有副作用都能被正確清理,避免內(nèi)存泄漏。
11. provide/inject 的響應(yīng)式傳遞
provide/inject 大家都知道,但很多人不知道它可以傳遞響應(yīng)式數(shù)據(jù),而且子組件可以直接修改。
// 父組件
import { provide, ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const user = reactive({ name: '張三', age: 20 })
// 提供響應(yīng)式數(shù)據(jù)
provide('count', count)
provide('user', user)
// 也可以提供修改函數(shù)
provide('increment', () => {
count.value++
})
return { count, user }
}
}
// 子組件(可以是任意深度的子組件)
import { inject, ref } from 'vue'
export default {
setup() {
// 注入響應(yīng)式數(shù)據(jù),可以設(shè)置默認(rèn)值
const count = inject('count', ref(0)) // 如果父組件沒(méi)有提供,使用默認(rèn)值
const user = inject('user', () => ({ name: '默認(rèn)用戶', age: 0 })) // 也可以使用工廠函數(shù)
const increment = inject('increment', () => {
console.warn('increment 方法未提供')
})
// 可以直接修改(如果父組件提供的是響應(yīng)式數(shù)據(jù))
const handleClick = () => {
count.value++ // ? 父組件的 count 也會(huì)更新
user.name = '李四' // ? 父組件的 user 也會(huì)更新
// 或者調(diào)用父組件提供的方法
increment()
}
return { count, user, handleClick }
}
}
這個(gè)在跨層級(jí)組件通信的時(shí)候特別方便,比 props 一層層傳要優(yōu)雅得多。
12. v-memo - 性能優(yōu)化的指令
v-memo 是 Vue3.2 新增的指令,可以緩存模板的一部分,只有在依賴項(xiàng)變化時(shí)才重新渲染。
<template>
<!-- 只有 item.id 或 selected 變化時(shí)才重新渲染這個(gè) div -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, selected]">
<p>{{ item.name }}</p>
<p>{{ item.description }}</p>
<!-- 復(fù)雜的內(nèi)容 -->
<ExpensiveComponent :data="item.data" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, name: 'Item 1', description: '...', data: {...} },
{ id: 2, name: 'Item 2', description: '...', data: {...} },
// ...
])
const selected = ref(null)
</script>
這個(gè)在處理長(zhǎng)列表的時(shí)候特別有用,可以大幅提升性能。不過(guò)要注意,v-memo 必須和 v-for 一起使用。
13. Suspense - 異步組件的加載狀態(tài)
Suspense 可以優(yōu)雅地處理異步組件的加載狀態(tài),不需要在每個(gè)組件里單獨(dú)處理。
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading">加載中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>
如果異步組件內(nèi)部有異步操作(比如 setup 里用了 await),Suspense 也會(huì)等待:
<!-- AsyncComponent.vue --> <script setup> // 這個(gè)組件內(nèi)部有異步操作 const data = await fetchData() // Suspense 會(huì)等待這個(gè)完成 </script>
這個(gè)在處理異步數(shù)據(jù)加載的時(shí)候特別優(yōu)雅,比手動(dòng)管理 loading 狀態(tài)要方便。
14. 模板引用的高級(jí)用法
模板引用(ref)除了獲取 DOM 元素,還有很多高級(jí)用法。
<template>
<!-- 在 v-for 中獲取多個(gè)元素 -->
<div v-for="item in list" :ref="el => setItemRef(el, item.id)">
{{ item.name }}
</div>
<!-- 獲取子組件實(shí)例 -->
<ChildComponent ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
])
// 存儲(chǔ)多個(gè)元素引用
const itemRefs = ref({})
const setItemRef = (el, id) => {
if (el) {
itemRefs.value[id] = el
}
}
// 子組件引用
const childRef = ref(null)
onMounted(() => {
// 訪問(wèn)子組件的方法或?qū)傩?
console.log(childRef.value.someMethod())
console.log(childRef.value.someProperty)
// 訪問(wèn)所有列表項(xiàng)的 DOM
console.log(itemRefs.value)
})
</script>
還可以配合 defineExpose 暴露子組件的特定內(nèi)容:
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const privateData = ref('private') // 這個(gè)不會(huì)被暴露
const increment = () => {
count.value++
}
// 只暴露 count 和 increment
defineExpose({
count,
increment
})
</script>
15. computed 的 getter/setter 形式
computed 除了只讀形式,還可以有 getter 和 setter,實(shí)現(xiàn)雙向綁定。
import { ref, computed } from 'vue'
const firstName = ref('張')
const lastName = ref('三')
// 可讀寫的 computed
const fullName = computed({
get() {
return `${firstName.value}${lastName.value}`
},
set(newValue) {
// 當(dāng)設(shè)置 fullName 時(shí),自動(dòng)拆分到 firstName 和 lastName
const parts = newValue.split('')
if (parts.length >= 2) {
firstName.value = parts[0]
lastName.value = parts.slice(1).join('')
}
}
})
// 使用
console.log(fullName.value) // '張三'
fullName.value = '李四' // 自動(dòng)更新 firstName 和 lastName
console.log(firstName.value) // '李'
console.log(lastName.value) // '四'
這個(gè)在處理表單數(shù)據(jù)轉(zhuǎn)換的時(shí)候特別有用。
16. watch 的深度監(jiān)聽(tīng)和立即執(zhí)行
watch 的選項(xiàng)參數(shù)有很多實(shí)用的配置。
import { watch, ref, reactive } from 'vue'
const count = ref(0)
const state = reactive({
user: {
name: '張三',
age: 20
}
})
// 深度監(jiān)聽(tīng)對(duì)象
watch(
() => state.user,
(newVal, oldVal) => {
console.log('user 變化了', newVal, oldVal)
},
{ deep: true } // 深度監(jiān)聽(tīng)嵌套屬性
)
// 或者直接監(jiān)聽(tīng)整個(gè)對(duì)象
watch(
state,
(newVal, oldVal) => {
// 注意:newVal 和 oldVal 是同一個(gè)對(duì)象(因?yàn)?reactive)
console.log('state 變化了')
},
{ deep: true }
)
// 立即執(zhí)行一次
watch(
count,
(newVal, oldVal) => {
console.log('count:', newVal)
},
{ immediate: true } // 立即執(zhí)行一次回調(diào)
)
// 監(jiān)聽(tīng)多個(gè)源
watch(
[count, () => state.user.name],
([newCount, newName], [oldCount, oldName]) => {
console.log('count 或 name 變化了')
}
)
// 手動(dòng)實(shí)現(xiàn)只監(jiān)聽(tīng)一次的效果
let stopWatcher = null
stopWatcher = watch(
count,
(newVal) => {
console.log('只執(zhí)行一次')
// 執(zhí)行后立即停止監(jiān)聽(tīng)
if (stopWatcher) {
stopWatcher()
stopWatcher = null
}
}
)
17. nextTick 的多種用法
nextTick 大家都知道,但它的使用場(chǎng)景可能很多人沒(méi)完全掌握。
import { nextTick, ref } from 'vue'
const count = ref(0)
const message = ref('')
// 等待 DOM 更新后執(zhí)行
async function handleClick() {
count.value++
message.value = 'Count 已更新'
// 此時(shí) DOM 可能還沒(méi)更新
console.log(document.querySelector('.count').textContent) // 可能是舊值
// 等待 DOM 更新
await nextTick()
console.log(document.querySelector('.count').textContent) // 新值
// 或者用回調(diào)形式
nextTick(() => {
console.log('DOM 已更新')
})
}
// 在組件掛載后操作 DOM
import { onMounted } from 'vue'
onMounted(async () => {
// 即使組件已掛載,某些 DOM 操作可能還需要等待
await nextTick()
// 現(xiàn)在可以安全地操作 DOM 了
document.querySelector('.some-element').focus()
})
18. 響應(yīng)式 API 的轉(zhuǎn)換工具
Vue3 提供了一些轉(zhuǎn)換工具,可以在不同響應(yīng)式類型之間轉(zhuǎn)換。
import { ref, reactive, toRef, toRefs, isRef } from 'vue'
const state = reactive({
name: 'Vue3',
version: '3.0',
count: 0
})
// toRef - 將 reactive 對(duì)象的某個(gè)屬性轉(zhuǎn)為 ref
const nameRef = toRef(state, 'name')
nameRef.value = 'Vue 3' // 會(huì)同步更新 state.name
// toRefs - 將 reactive 對(duì)象的所有屬性轉(zhuǎn)為 ref
const { name, version, count } = toRefs(state)
// 現(xiàn)在 name、version、count 都是 ref,可以在模板中直接使用
// 在組合式函數(shù)中返回響應(yīng)式數(shù)據(jù)時(shí)特別有用
function useCounter() {
const state = reactive({
count: 0,
step: 1
})
const increment = () => {
state.count += state.step
}
// 返回 toRefs,讓使用者可以解構(gòu)而不失去響應(yīng)式
return {
...toRefs(state),
increment
}
}
// 使用時(shí)可以解構(gòu)
const { count, step, increment } = useCounter()
// count 和 step 仍然是響應(yīng)式的
19. 自定義指令的高級(jí)用法
自定義指令除了基本的 mounted 和 updated,還有很多生命周期鉤子。
// 自定義指令直接定義對(duì)象即可,不需要特殊導(dǎo)入
const vMyDirective = {
// 元素插入 DOM 前調(diào)用
created(el, binding, vnode) {
console.log('created')
},
// 元素插入 DOM 后調(diào)用
mounted(el, binding, vnode, prevVnode) {
console.log('mounted', binding.value)
// binding.value - 指令的值
// binding.oldValue - 之前的值(僅在 update 和 componentUpdated 中可用)
// binding.arg - 指令參數(shù),如 v-my-directive:foo 中的 'foo'
// binding.modifiers - 修飾符對(duì)象,如 v-my-directive.foo.bar 中的 { foo: true, bar: true }
},
// 在包含組件的 VNode 更新前調(diào)用
beforeUpdate(el, binding, vnode, prevVnode) {
console.log('beforeUpdate')
},
// 在包含組件的 VNode 及其子 VNode 更新后調(diào)用
updated(el, binding, vnode, prevVnode) {
console.log('updated')
},
// 在卸載前調(diào)用
beforeUnmount(el, binding, vnode) {
console.log('beforeUnmount')
},
// 卸載后調(diào)用
unmounted(el, binding, vnode) {
console.log('unmounted')
}
}
// 使用
// <div v-my-directive:arg.modifier="value"></div>
實(shí)際應(yīng)用:實(shí)現(xiàn)一個(gè)點(diǎn)擊外部關(guān)閉的指令
const vClickOutside = {
mounted(el, binding) {
el._clickOutside = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value() // 調(diào)用傳入的函數(shù)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
}
20. 組合式 API 的類型推斷技巧
在 TypeScript 中使用組合式 API 時(shí),有一些類型推斷的技巧。
import { ref, computed, Ref, ComputedRef } from 'vue'
// 明確指定 ref 的類型
const count: Ref<number> = ref(0)
// 或者讓 TypeScript 自動(dòng)推斷
const count = ref<number>(0)
// computed 的類型
const doubleCount: ComputedRef<number> = computed(() => count.value * 2)
// 在組合式函數(shù)中返回類型
function useCounter(initialValue: number = 0) {
const count = ref(initialValue)
const increment = () => {
count.value++
}
return {
count: count as Ref<number>, // 或者用 readonly
increment
}
}
// 使用泛型
function useAsyncData<T>(url: string) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return {
data: readonly(data),
loading: readonly(loading),
error: readonly(error),
fetchData
}
}
21. 性能優(yōu)化的組合拳
把這些 API 組合起來(lái),可以實(shí)現(xiàn)更強(qiáng)大的性能優(yōu)化。
import { shallowRef, markRaw, computed, watchEffect } from 'vue'
// 場(chǎng)景:處理大量數(shù)據(jù)列表
function useLargeList(initialData) {
// 用 shallowRef 避免深度響應(yīng)式
const items = shallowRef(initialData.map(item => ({
...item,
// 某些不需要響應(yīng)式的數(shù)據(jù)用 markRaw
metadata: markRaw(item.metadata)
})))
// 用 computed 緩存計(jì)算結(jié)果
const filteredItems = computed(() => {
return items.value.filter(item => item.visible)
})
// 用 watchEffect 處理副作用,自動(dòng)清理
const stop = watchEffect((onInvalidate) => {
// 處理過(guò)濾后的數(shù)據(jù)
processItems(filteredItems.value)
onInvalidate(() => {
// 清理工作
cleanup()
})
})
return {
items,
filteredItems,
stop
}
}
22. 實(shí)際項(xiàng)目中的最佳實(shí)踐
結(jié)合我自己的項(xiàng)目經(jīng)驗(yàn),分享幾個(gè)實(shí)用的模式:
模式1:響應(yīng)式數(shù)據(jù)的初始化
import { ref, reactive, onMounted, readonly } from 'vue'
function useUserData(userId) {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchUser = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${userId}`)
user.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// 組件掛載時(shí)自動(dòng)獲取
onMounted(fetchUser)
return {
user: readonly(user),
loading: readonly(loading),
error: readonly(error),
refetch: fetchUser
}
}
模式2:表單驗(yàn)證
import { ref, computed, reactive, readonly } from 'vue'
function useFormValidation() {
const form = reactive({
email: '',
password: '',
confirmPassword: ''
})
const errors = reactive({})
const validateEmail = (email) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
// 用 computed 實(shí)時(shí)驗(yàn)證
const isValid = computed(() => {
errors.email = form.email && !validateEmail(form.email)
? '郵箱格式不正確'
: ''
errors.password = form.password.length < 6
? '密碼至少6位'
: ''
errors.confirmPassword = form.password !== form.confirmPassword
? '兩次密碼不一致'
: ''
return !Object.values(errors).some(err => err)
})
return {
form,
errors: readonly(errors),
isValid
}
}
模式3:防抖和節(jié)流的組合使用
import { customRef, watch } from 'vue'
// 防抖 ref - 基于 customRef 實(shí)現(xiàn)
function useDebounceRef(value, delay = 300) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
// 節(jié)流函數(shù) - 配合 watch 使用(這是純 JS 函數(shù),不是 Vue3 API,但經(jīng)常和 Vue3 一起用)
function useThrottleFn(fn, delay = 300) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
fn.apply(this, args)
lastTime = now
}
}
}
// 使用
const searchText = useDebounceRef('')
watch(searchText, (newVal) => {
// 搜索邏輯,只在用戶停止輸入 300ms 后執(zhí)行
performSearch(newVal)
})
const handleScroll = useThrottleFn(() => {
// 滾動(dòng)處理,每 300ms 最多執(zhí)行一次
updateScrollPosition()
}, 100)
版本要求說(shuō)明
部分 API 需要特定版本的 Vue3:
- Vue 3.2+:
effectScope、onScopeDispose、v-memo - Vue 3.0+:其他大部分 API
建議使用 Vue 3.2 或更高版本,以獲得完整的 API 支持。
常見(jiàn)陷阱和注意事項(xiàng)
shallowRef/shallowReactive 的陷阱:修改嵌套屬性不會(huì)觸發(fā)更新,如果需要更新,要替換整個(gè)對(duì)象。
toRaw 的局限性:只能獲取一層的原始對(duì)象,嵌套的響應(yīng)式對(duì)象仍然是 Proxy。
getCurrentInstance 的使用限制:只能在
setup或生命周期鉤子中調(diào)用,不能緩存到異步回調(diào)中使用。inject 的默認(rèn)值:如果父組件沒(méi)有提供值,
inject會(huì)返回undefined,建議總是提供默認(rèn)值。watchEffect 的依賴追蹤:會(huì)自動(dòng)追蹤所有在函數(shù)內(nèi)訪問(wèn)的響應(yīng)式數(shù)據(jù),注意不要訪問(wèn)不需要追蹤的數(shù)據(jù)。
computed 的副作用:不要在
computed中執(zhí)行副作用操作(如修改 DOM、發(fā)送請(qǐng)求等),應(yīng)該使用watch或watchEffect。
總結(jié)下
有些API你可能不常用,但在特定場(chǎng)景下真的能解決實(shí)際問(wèn)題。性能優(yōu)化、邊界情況處理、自定義邏輯,這些場(chǎng)景下它們都能派上用場(chǎng)。
不過(guò)也要注意,不要過(guò)度使用。比如 shallowRef 雖然性能好,但如果你確實(shí)需要深度響應(yīng)式,就不要為了性能而犧牲功能。工具是死的,場(chǎng)景是活的,根據(jù)實(shí)際需求選擇最合適的方案才是王道。。。
到此這篇關(guān)于vue3一些不常用卻很實(shí)用API的文章就介紹到這了,更多相關(guān)vue3實(shí)用API內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中this.$refs有值,但無(wú)法獲取ref的值問(wèn)題及解決
這篇文章主要介紹了vue中this.$refs有值,但無(wú)法獲取ref的值問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
vue一個(gè)頁(yè)面實(shí)現(xiàn)音樂(lè)播放器的示例
這篇文章主要介紹了vue一個(gè)頁(yè)面實(shí)現(xiàn)音樂(lè)播放器的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
vue播放flv、m3u8視頻流(監(jiān)控)的方法實(shí)例
隨著前端大屏頁(yè)面的逐漸壯大,客戶的...其中實(shí)時(shí)播放監(jiān)控的需求逐步增加,視頻流格式也是有很多種,用到最多的.flv、.m3u8,下面這篇文章主要給大家介紹了關(guān)于vue播放flv、m3u8視頻流(監(jiān)控)的相關(guān)資料,需要的朋友可以參考下2023-04-04
iview實(shí)現(xiàn)select tree樹(shù)形下拉框的示例代碼
這篇文章主要介紹了iview實(shí)現(xiàn)select tree樹(shù)形下拉框的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12
Vue.js中使用iView日期選擇器并設(shè)置開(kāi)始時(shí)間結(jié)束時(shí)間校驗(yàn)功能
本文通過(guò)實(shí)例代碼給大家介紹了Vue.js中使用iView日期選擇器并設(shè)置開(kāi)始時(shí)間結(jié)束時(shí)間校驗(yàn)功能,需要的朋友可以參考下2018-08-08
vue項(xiàng)目中如何調(diào)用多個(gè)不同的ip接口
這篇文章主要介紹了vue項(xiàng)目中如何調(diào)用多個(gè)不同的ip接口,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
Vue.set()動(dòng)態(tài)的新增與修改數(shù)據(jù),觸發(fā)視圖更新的方法
今天小編就為大家分享一篇Vue.set()動(dòng)態(tài)的新增與修改數(shù)據(jù),觸發(fā)視圖更新的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
rollup3.x+vue2打包組件的實(shí)現(xiàn)
本文主要介紹了rollup3.x+vue2打包組件的實(shí)現(xiàn),詳細(xì)的介紹了打包會(huì)存在的問(wèn)題,包版本的問(wèn)題,babel 轉(zhuǎn)換jsx等問(wèn)題,具有一定的參考價(jià)值,感興趣的可以了解一下2023-03-03
elementUI?checkBox報(bào)錯(cuò)Cannot read property &ap
這篇文章主要為大家介紹了elementUI?checkBox報(bào)錯(cuò)Cannot read property 'length' of undefined的解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06

