从定时器管理出发,彻底搞懂防抖与节流的实现逻辑

在前端开发中,高频事件(如输入、滚动、窗口缩放)若不加控制,极易引发性能问题。为应对这一挑战,防抖(debounce)节流(throttle) 成为必备工具。


一、防抖:每次触发都重置定时器

假设我们要实现一个功能:用户在输入框打字时,只有当他停止输入超过 1 秒,才发送请求。

第一步:我需要延迟执行

显然,要用 setTimeout

scss 复制代码
setTimeout(() => {
    ajax(value);
}, 1000);

第二步:但如果用户继续输入,之前的请求就不该发

→ 所以必须取消之前的定时器,再建一个新的。

这就要求我们保存定时器 ID

scss 复制代码
let timerId;
// 每次触发时:
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
    ajax(value);
}, 1000);

第三步:处理 this 和参数

因为 ajax 可能依赖上下文或多个参数,不能直接写死。我们需要在触发时捕获当前的 thisarguments

javascript 复制代码
function debounce(fn, delay) {
    let timerId;
    return function(...args) {
        const context = this;
        if (timerId) clearTimeout(timerId);
        timerId = setTimeout(() => {
            fn.apply(context, args);
        }, delay);
    };
}

到此,防抖完成。它的全部逻辑就源于一句话: "每次触发,先删旧定时器,再建新定时器。"

所谓"停手后执行",只是这种操作的自然结果。


二、节流:控制执行频率,必要时预约补发

现在需求变了:不管用户多快输入,每 1 秒最多只发一次请求,且最后一次输入不能丢

第一步:我能立即执行吗?

用时间戳判断是否已过 delay

ini 复制代码
const now = Date.now();
if (now - last >= delay) {
    fn(); 
    last = now; // 记录执行时间
}

这能保证最小间隔,但有个致命缺陷:如果用户快速输入后立刻停止,最后一次可能永远不会执行。

第二步:如何不丢尾?

→ 在冷却期内,预约一次未来的执行 。这就要用到 setTimeout

于是逻辑分裂为两条路径:

  • 路径 A(可立即执行) :时间到了,马上执行,更新 last
  • 路径 B(还在冷却) :清除之前的预约,重新预约一次执行

第三步:管理预约定时器

我们需要一个变量 deferTimer 来保存预约任务的 ID:

ini 复制代码
let last = 0;
let deferTimer = null;

当处于冷却期时:

scss 复制代码
clearTimeout(deferTimer); // 清除旧预约
deferTimer = setTimeout(() => {
    last = Date.now(); // 关键:这次执行也要记录时间!
    fn.apply(this, args);
}, delay - (now - last)); // 精确计算剩余等待时间

第四步:整合逻辑

ini 复制代码
function throttle(fn, delay) {
    let last = 0;
    let deferTimer = null;

    return function(...args) {
        const context = this;
        const now = Date.now();

        if (now - last >= delay) {
            // 路径 A:立即执行
           
            last = now;
            fn.apply(context, args);
        } else {
            // 路径 B:预约执行
            if (deferTimer) clearTimeout(deferTimer);
            deferTimer = setTimeout(() => {
                last = Date.now(); // 必须更新!
               
                fn.apply(context, args);
            }, delay - (now - last));
        }
    };
}

节流的核心,是两种执行方式的协同

  • 立即执行靠时间戳判断
  • 补发执行靠 setTimeout 预约
    而两者共享同一个 last 状态,确保整体节奏不乱。

三、对比总结:防抖 vs 节流的机制差异

维度 防抖(Debounce) 节流(Throttle)
核心操作 每次触发都 clearTimeout + setTimeout 冷却期内 clearTimeout + setTimeout,否则立即执行
状态变量 仅需 timerId last(时间) + deferTimer(预约ID)
执行特点 只执行最后一次 固定间隔执行,且不丢尾
适用场景 搜索建议、表单校验 滚动加载、按钮限频、实时位置上报
相关推荐
SoaringHeart19 小时前
Flutter调试组件:打印任意组件尺寸位置信息 NRenderBox
前端·flutter
晚风予星20 小时前
Ant Design Token Lens 迎来了全面升级!支持在 .tsx 或 .ts 文件中直接使用 Design Token
前端·react.js·visual studio code
sunny_20 小时前
⚡️ vite-plugin-oxc:从 Babel 到 Oxc,我为 Vite 写了一个高性能编译插件
前端·webpack·架构
GIS之路20 小时前
ArcPy 开发环境搭建
前端
林小帅1 天前
【笔记】OpenClaw 架构浅析
前端·agent
林小帅1 天前
【笔记】OpenClaw 生态系统的多语言实现对比分析
前端·agent
程序猿的程1 天前
开源一个 React 股票 K 线图组件,传个股票代码就能画图
前端·javascript
不爱说话郭德纲1 天前
告别漫长的HbuilderX云打包排队!uni-app x 安卓本地打包保姆级教程(附白屏、包体积过大排坑指南)
android·前端·uni-app
大雨还洅下1 天前
前端JS: 虚拟dom是什么? 原理? 优缺点?
javascript
唐叔在学习1 天前
[前端特效] 左滑显示按钮的实现介绍
前端·javascript