Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀

一、前言

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 生态接轨的约定

  1. 命名即契约:看到 use 前缀,调用方立刻知道"该在 setup 里同步调用、返回的东西接入了响应式体系"
  2. 留了升级空间:将来想加"组件卸载自动 cancel"(内部注册 onBeforeUnmount),use 形态才能做到------普通工具函数拿不到组件实例
  3. 与 VueUse 无缝替换 :官方 VueUse 库(079 篇)提供了同签名 useDebounceFn / useThrottleFn,今天手写的版本以后一行 import 就能换成生产版------API 设计对齐,迁移零成本

六、踩坑记录

  1. 防抖写成 setTimeout(fn(), delay) :立即执行了 fn(括号丢了延迟)------传函数本身或箭头包裹 () => fn(args)
  2. this / args 丢失debounced = () => fn() 把 this 和参数全丢了------用普通函数 + ...args + fn.apply(this, args) 全量透传
  3. 组件卸载没 cancel:卸载瞬间还有一次"排队中"的防抖任务,执行时组件 DOM 已销毁引发报错------onBeforeUnmount 里调 cancel
  4. 节流首次不执行的困扰:想要"先执行再等"用本文版(时间戳法);想要"等够间隔才第一次执行"需要定时器版变体------两种语义按需选,先明确需求
  5. 给 computed 里的逻辑做防抖 :computed 必须同步返回,不能塞异步/延迟------防抖放在事件的源头(如 @input 处理函数),别塞在计算链条中间

七、今日小结

  • 防抖:clearTimeout + setTimeout 重新排队,"停下来才执行",附 cancel 清理
  • 节流:时间戳间隔判断,"固定频率放行",首次立即执行
  • 选型:要最终结果 → 防抖;要持续反馈 → 节流
  • use 前缀包装的对齐价值:命名契约、可升级、可无缝替换 VueUse

下篇预告

组合式函数篇收官:从这五篇的实战里提炼团队级最佳实践------命名规范、参数设计(怎么优雅地接收响应式参数)、返回值设计(toRefs 何时登场)、目录与文档约定。下一篇 039,让你写的组合式函数在团队里"免检通行"。

相关推荐
JavaGuide1 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马1 小时前
AI驱动自动化业务工作流搭建实践
前端
码云之上2 小时前
Skill 里的脚本终于能跑了,星悟接 CubeSandbox 的纪实
前端·人工智能·前端框架
IT_陈寒2 小时前
Vue computed属性这个坑,我居然踩了三次才爬出来
前端·人工智能·后端
烈风逍遥2 小时前
第三篇:组件化实践,SpTable 通用表格组件设计
前端·架构
计算机魔术师2 小时前
三周时间,128个PR,83万行代码——GitHub 是怎么让 AI 重写自己的
前端
甲维斯3 小时前
完蛋了,豆包变这么强?Seed2.1pro小测一下!
前端·人工智能
烈风逍遥3 小时前
第一篇:SeaPack 全栈项目工程化实践
前端·后端·架构