一、前言
009 篇搜索联想留了个尾巴:连续快速输入会发一堆请求。今天还账------亲手封装 useDebounceFn(防抖) 和 useThrottleFn(节流)。这两个函数是前端面试手写题 Top 3,也是性能优化的万金油。别背八股,跟着场景把原理写出来。
二、防抖:等你说完我再动
场景:搜索框连续输入,只想要"停下来之后"的那一次请求。
防抖策略:事件触发后 delay 毫秒才执行;期间再触发,计时器推倒重来------像电梯关门,有人进来就重新等。
js
// src/composables/useDebounceFn.js
/**
* 防抖:停止触发 delay 毫秒后,才真正执行一次
* @param {Function} fn 要防抖的函数
* @param {number} delay 等待毫秒数
* @returns {Function} 防抖后的函数(附 cancel 方法)
*/
export function useDebounceFn(fn, delay = 300) {
let timer = null
// 必须是普通函数:保留 this,且每次调用独立处理 args
function debounced(...args) {
clearTimeout(timer) // 1. 掐掉上一次的等待
timer = setTimeout(() => {
fn.apply(this, args) // 2. 安静 delay 毫秒后执行
}, delay)
}
// 手动取消:组件卸载时清理"还在排队"的任务
debounced.cancel = () => {
clearTimeout(timer)
timer = null
}
return debounced
}
实现只有两行核心:clearTimeout 掉旧的,setTimeout 一个新的。每次触发都"重新排队",只有最后一次能活到执行时刻。
vue
<!-- 实战:搜索联想 -->
<template>
<input v-model.trim="keyword" placeholder="输入关键词(停 300ms 才搜索)" />
<p>已发起搜索次数:{{ searchCount }}</p>
<ul>
<li v-for="item in result" :key="item">{{ item }}</li>
</ul>
</template>
<script setup>
import { ref, onBeforeUnmount } from 'vue'
import { useDebounceFn } from '@/composables/useDebounceFn'
const keyword = ref('')
const result = ref([])
const searchCount = ref(0)
function fetchSearch(kw) {
searchCount.value++
// 模拟接口
result.value = [`Vue3 ${kw}`, `Vite ${kw}`, `${kw} 教程`]
}
// 包装:输入事件先过防抖阀
const onInput = useDebounceFn(() => {
if (!keyword.value) {
result.value = []
return
}
fetchSearch(keyword.value)
}, 300)
// 组件卸载时,取消还在排队的搜索
onBeforeUnmount(() => onInput.cancel())
</script>
快速连打 10 个字符:事件触发了 10 次,但真正执行只有 1 次(次数计数器看得见)。009 篇的问题正式关闭。
【截图位置:连打字符后 searchCount 只加 1 的效果】
三、节流:再急也按固定频率来
场景 :滚动加载、resize 计算、按钮防连点------事件一直在高频触发,我们想要的不是"最后一次",而是每隔一段时间来一次。
节流策略:两次执行之间强制间隔 interval 毫秒------像水龙头的节流阀,不管水压多大,出水频率恒定。
js
// src/composables/useThrottleFn.js
/**
* 节流:两次执行之间至少间隔 interval 毫秒(首次立即执行)
* @param {Function} fn 要节流的函数
* @param {number} interval 最小间隔毫秒数
* @returns {Function} 节流后的函数
*/
export function useThrottleFn(fn, interval = 500) {
let lastTime = 0
function throttled(...args) {
const now = Date.now()
if (now - lastTime >= interval) { // 距上次执行够久了才放行
lastTime = now
fn.apply(this, args)
}
}
return throttled
}
核心就一个时间戳比较。注意这里首次必然执行(lastTime 初始 0,now - 0 必然够大)------"立刻响应第一次"通常正是想要的体验。
vue
<!-- 实战 1:按钮防连点 -->
<template>
<p>500ms 内点再多次,也只加 1:</p>
<button @click="addThrottled">疯狂点击加血</button>
<p>HP:{{ hp }}</p>
</template>
<script setup>
import { ref } from 'vue'
import { useThrottleFn } from '@/composables/useThrottleFn'
const hp = ref(100)
const addThrottled = useThrottleFn(() => {
hp.value = Math.min(100, hp.value + 10)
}, 500)
</script>
vue
<!-- 实战 2:滚动加载(搭配 037 篇 useScrollY) -->
<template>
<p>滚动位置:{{ y }}(节流后每 500ms 才检查一次)</p>
<p v-if="nearBottom">触底了,加载更多...</p>
<div style="height: 1500px">长页面占位</div>
</template>
<script setup>
import { computed } from 'vue'
import { useScrollY } from '@/composables/useScrollY'
import { useThrottleFn } from '@/composables/useThrottleFn'
const { y } = useScrollY()
// 组合式函数叠罗汉:滚动值先经节流再参与计算
const nearBottom = computed(() => y.value + window.innerHeight >= 1450)
</script>
四、防抖 vs 节流:一张表定终身
| 对比项 | 防抖 debounce | 节流 throttle |
|---|---|---|
| 策略 | 停下来才执行(重新计时) | 固定频率执行(无视触发多猛) |
| 连续触发时 | 只执行最后一次 | 每隔 interval 执行一次 |
| 类比 | 电梯关门,有人就重等 | 红绿灯,按固定间隔放行 |
| 典型场景 | 搜索输入、表单校验、resize 完成后计算 | 滚动加载、鼠标移动跟随、按钮防连点 |
选型口诀:"我只要最终结果" → 防抖;"我要持续反馈但不能太密" → 节流。分不清就问自己:停顿后要不要补一次执行?要 → 防抖(节流停了就不会再触发)。
【截图位置:疯狂点击按钮、HP 缓慢上涨的效果】
五、为什么包一层 use 前缀
直接写个 debounce(fn, delay) 工具函数不行吗?行,而且很多工具库就是这么干的。包成 useDebounceFn 的增量价值在于和 Vue 生态接轨的约定:
- 命名即契约:看到 use 前缀,调用方立刻知道"该在 setup 里同步调用、返回的东西接入了响应式体系"
- 留了升级空间:将来想加"组件卸载自动 cancel"(内部注册 onBeforeUnmount),use 形态才能做到------普通工具函数拿不到组件实例
- 与 VueUse 无缝替换 :官方 VueUse 库(079 篇)提供了同签名 useDebounceFn / useThrottleFn,今天手写的版本以后一行 import 就能换成生产版------API 设计对齐,迁移零成本
六、踩坑记录
- 防抖写成
setTimeout(fn(), delay):立即执行了 fn(括号丢了延迟)------传函数本身或箭头包裹() => fn(args) - this / args 丢失 :
debounced = () => fn()把 this 和参数全丢了------用普通函数 +...args+fn.apply(this, args)全量透传 - 组件卸载没 cancel:卸载瞬间还有一次"排队中"的防抖任务,执行时组件 DOM 已销毁引发报错------onBeforeUnmount 里调 cancel
- 节流首次不执行的困扰:想要"先执行再等"用本文版(时间戳法);想要"等够间隔才第一次执行"需要定时器版变体------两种语义按需选,先明确需求
- 给 computed 里的逻辑做防抖 :computed 必须同步返回,不能塞异步/延迟------防抖放在事件的源头(如 @input 处理函数),别塞在计算链条中间
七、今日小结
- 防抖:clearTimeout + setTimeout 重新排队,"停下来才执行",附 cancel 清理
- 节流:时间戳间隔判断,"固定频率放行",首次立即执行
- 选型:要最终结果 → 防抖;要持续反馈 → 节流
- use 前缀包装的对齐价值:命名契约、可升级、可无缝替换 VueUse
下篇预告
组合式函数篇收官:从这五篇的实战里提炼团队级最佳实践------命名规范、参数设计(怎么优雅地接收响应式参数)、返回值设计(toRefs 何时登场)、目录与文档约定。下一篇 039,让你写的组合式函数在团队里"免检通行"。