Vue3 的 watch,性能真的很差!你們知道怎么優(yōu)化嗎?
最近發(fā)現(xiàn)公司某個(gè) Vue 頁(yè)面非常卡,一看原來(lái)是因?yàn)轫?yè)面中使用了大量的 watch ,但是又沒(méi)有進(jìn)行防抖或者節(jié)流的限制,導(dǎo)致了觸發(fā)非常頻繁,所以頁(yè)面非??D。
所以想著,想分享一下如何給 watch 加上 防抖、節(jié)流。
一、前置知識(shí):防抖與節(jié)流
在實(shí)現(xiàn)監(jiān)聽(tīng)器之前,我們需要理解兩個(gè)核心概念:
- 防抖(Debounce): 在連續(xù)觸發(fā)時(shí),只在最后一次操作后等待指定時(shí)間執(zhí)行
- 節(jié)流(Throttle): 在連續(xù)觸發(fā)時(shí),保證固定時(shí)間間隔內(nèi)只執(zhí)行一次
二、基礎(chǔ)監(jiān)聽(tīng)器實(shí)現(xiàn)
我們先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的響應(yīng)式監(jiān)聽(tīng)器,基于Vue的watch函數(shù):
/**
* 基礎(chǔ)監(jiān)聽(tīng)器
* @param {Ref} source 要監(jiān)聽(tīng)的響應(yīng)式數(shù)據(jù)
* @param {Function} callback 變化回調(diào)
* @param {Object} options 監(jiān)聽(tīng)選項(xiàng)
*/
function basicWatcher(source, callback, options = {}) {
let cleanup = () => {}
const stop = watch(source, (value, oldValue, onCleanup) => {
// 清除之前的副作用
cleanup()
// 注冊(cè)新的清理函數(shù)
onCleanup(() => {
cleanup = () => {}
})
// 執(zhí)行回調(diào)
callback(value, oldValue)
}, options)
return stop
}
三、實(shí)現(xiàn)防抖監(jiān)聽(tīng)器(watchDebounced)
實(shí)現(xiàn)要點(diǎn):
- 使用 setTimeout 延遲回調(diào)執(zhí)行
- 每次新變化時(shí)重置定時(shí)器
- 清理函數(shù)確保組件卸載時(shí)終止等待中的回調(diào)
/**
* 防抖監(jiān)聽(tīng)器
* @param {Ref} source 要監(jiān)聽(tīng)的響應(yīng)式數(shù)據(jù)
* @param {Function} callback 回調(diào)函數(shù)
* @param {number} delay 防抖延遲時(shí)間(毫秒)
* @param {Object} options 監(jiān)聽(tīng)選項(xiàng)
*/
function watchDebounced(source, callback, delay = 300, options = {}) {
let timeoutId = null
let cleanup = () => {}
const stop = watch(source, (value, oldValue, onCleanup) => {
// 清除之前的定時(shí)器和副作用
clearTimeout(timeoutId)
cleanup()
// 設(shè)置新的定時(shí)器
timeoutId = setTimeout(() => {
// 執(zhí)行回調(diào)時(shí)綁定正確的this上下文
callback.call(this, value, oldValue)
}, delay)
// 注冊(cè)清理函數(shù)
onCleanup(() => {
clearTimeout(timeoutId)
cleanup = () => {}
})
}, options)
return stop
}
四、實(shí)現(xiàn)節(jié)流監(jiān)聽(tīng)器(watchThrottled)
實(shí)現(xiàn)要點(diǎn):
- 使用時(shí)間戳計(jì)算剩余可執(zhí)行時(shí)間
- 未到間隔時(shí)間時(shí),設(shè)置剩余時(shí)間的定時(shí)器
- 保證間隔時(shí)間內(nèi)至少執(zhí)行一次
/**
* 節(jié)流監(jiān)聽(tīng)器
* @param {Ref} source 要監(jiān)聽(tīng)的響應(yīng)式數(shù)據(jù)
* @param {Function} callback 回調(diào)函數(shù)
* @param {number} interval 節(jié)流間隔(毫秒)
* @param {Object} options 監(jiān)聽(tīng)選項(xiàng)
*/
function watchThrottled(source, callback, interval = 300, options = {}) {
let lastExecTime = 0
let cleanup = () => {}
let timeoutId = null
const stop = watch(source, (value, oldValue, onCleanup) => {
const now = Date.now()
const elapsed = now - lastExecTime
// 清除等待中的定時(shí)器
clearTimeout(timeoutId)
cleanup()
if (elapsed >= interval) {
// 立即執(zhí)行
callback(value, oldValue)
lastExecTime = now
} else {
// 設(shè)置剩余時(shí)間的定時(shí)器
timeoutId = setTimeout(() => {
callback(value, oldValue)
lastExecTime = Date.now()
}, interval - elapsed)
}
onCleanup(() => {
clearTimeout(timeoutId)
cleanup = () => {}
})
}, options)
return stop
}
五、使用示例
<script setup>
import { ref } from 'vue'
const searchKeyword = ref('')
// 防抖監(jiān)聽(tīng)示例
watchDebounced(
searchKeyword,
(newVal) => {
console.log('防抖搜索:', newVal)
// 這里可以執(zhí)行API請(qǐng)求
},
500
)
// 節(jié)流監(jiān)聽(tīng)示例
watchThrottled(
searchKeyword,
(newVal) => {
console.log('節(jié)流記錄:', newVal)
// 這里可以執(zhí)行高頻狀態(tài)記錄
},
1000
)
</script>