拒绝“帕金森”式请求:一文彻底搞懂防抖与节流

51 阅读5分钟

在前端开发中,我们经常遇到这样的场景:用户在搜索框疯狂输入,或者疯狂滚动页面。如果我们不对这些行为加以限制,浏览器就会像得了“帕金森”一样,不仅控制台里 AJAX 请求满天飞,页面也可能因为频繁重绘而卡顿。

这时候,就需要请出闭包应用的两尊大神:防抖 (Debounce)节流 (Throttle)

它们虽然是“亲兄弟”,也是性能优化的黄金搭档,但性格却截然不同。今天我们就结合实战代码,来彻底搞懂它们。

一、 为什么我们需要它们?

想象一下以下两个场景:

  1. 搜索建议 (Code Suggest / Baidu Ajax): 用户想搜 "JavaScript",每敲一个字母触发一次 keyup。如果没有限制,输入10个字符就会发10次请求。前9次都是没用的,既浪费带宽,又因为请求返回顺序的不确定性导致数据错乱。
  2. 页面滚动/拖拽: 用户滚动页面加载更多。scroll 事件触发频率极高(可能几毫秒一次)。如果在回调里做复杂计算或 DOM 操作,FPS 会瞬间掉底,用户体验极差。

核心问题: 事件触发频率太高,超过了浏览器的处理能力或业务需求。

解决方案: 利用 闭包 和 定时器,控制函数执行的频率。


二、 防抖 (Debounce):最后一次才是真爱

核心口诀: “管你触发多少次,我只认最后一次。”

1. 场景与比喻

生活中的例子: 就像坐电梯。

一个人进来了,电梯门开了。电梯准备关门(设置延时)。如果在关门前又进来一个人,电梯门得重新打开,倒计时重新开始。只有当没人再进来(一定时间内无操作),电梯门才会真正关上运行。

技术场景:

  • 搜索框输入(用户输完才发请求)。
  • 窗口大小调整(resize),调整完窗口后再计算布局。

2. 代码实现

防抖的精髓在于:每次触发事件,都清除上一次的定时器,重新开始倒计时。

JavaScript

// 防抖函数:闭包的高阶应用
function debounce(fn, delay) {
    // 1. 创建一个变量用来保存定时器ID
    // 这个变量存在于闭包中,不会被垃圾回收,所有的调用共享这一个 timer
    let timer = null; 
    
    return function() {
        // 保存当前的上下文 this 和参数 args
        // 否则在 setTimeout 中 this 会指向 window
        let that = this;
        let args = arguments;

        // 2. 如果之前已经有定时器了,说明上一次操作还没结束,赶紧清除掉!
        if (timer) clearTimeout(timer);

        // 3. 开启一个新的定时器,重新倒计时
        timer = setTimeout(function() {
            fn.apply(that, args); // 执行真正的业务逻辑
        }, delay);
    }
}

3. 效果

用户一直按键盘,timer 一直被 clear 也就是一直被推迟。只有用户停手 delay 毫秒后,函数才会执行。


三、 节流 (Throttle):天下武功,唯快不破(但有冷却时间)

核心口诀: “在该段时间内,无论你怎么点,我也只执行一次。”

1. 场景与比喻

生活中的例子: FPS 游戏的射速。

即使你拿着鼠标疯狂连点(触发事件),AK-47 的射速(执行频率)也是固定的。子弹射出后需要“冷却时间” (CD),CD 转好之前,你点烂鼠标也射不出子弹。

技术场景:

  • 滚动加载(Scroll Loading):不需要每像素都检查,每隔 200ms 检查一次滚动位置即可。
  • 高频点击提交:防止用户疯狂点击按钮提交表单。

2. 代码实现

节流的精髓在于:利用时间戳或定时器,判断当前时间是否已经超过了规定的“冷却时间”。

这里我们采用一个综合版(结合了时间戳和定时器),保证首发响应快,结尾也能补一次。

JavaScript

// 节流函数:FPS 游戏射速控制器
function throttle(fn, delay) {
    let last = 0;         // 上次触发的时间戳
    let deferTimer = null; // 用于处理最后一次的定时器

    return function() {
        let that = this;
        let args = arguments;
        let now = +new Date(); // 获取当前毫秒数

        // 判断:距离上次执行是否超过了 delay (CD是否转好了?)
        if (last && now < last + delay) {
            // 情况A: 还没到时间 (CD中)
            // 设置一个定时器,保证如果用户停止操作,最后一次也能被执行
            clearTimeout(deferTimer);
            deferTimer = setTimeout(function() {
                last = now;
                fn.apply(that, args);
            }, delay);
        } else {
            // 情况B: 时间到了 (CD好了) 或者 第一次触发
            last = now; // 更新上次执行时间
            fn.apply(that, args); // 立即开火!
        }
    }
}

四、 深度总结:防抖 vs 节流

特性防抖 (Debounce)节流 (Throttle)
核心逻辑延时执行。只要你一直动,我就一直不执行。间隔执行。无论你动多快,我按自己的节奏来。
比喻电梯关门 / 英雄回城 (被打断就重来)游戏射速 / 技能冷却 (CD)
最佳场景搜索框输入、文本自动保存页面滚动、窗口调整、鼠标移动事件
闭包作用存储 timer ID,确保能清除上一次存储 last 时间戳,确保能计算时间差

闭包在这里扮演了什么角色?

在 debounce 和 throttle 函数中,变量 timer 和 last 都是定义在外部函数中的。

当返回的内部函数被执行时,它们依然能访问这些变量。这就是闭包。

如果没有闭包,我们就需要把 timer 定义在全局变量里,这会污染全局作用域,而且如果有两个输入框都需要防抖,全局变量就会冲突。闭包完美解决了这个问题,让每个防抖函数都有自己独立的“私有变量”。


五、 实战演示

我们将两种策略挂载到 keyup 事件上对比一下:

  • 普通 Ajax:输入 hello,控制台打印 5 次。
  • 防抖 Ajax:输入 hello,中间不打印,停手后打印 1 次(最后一次)。
  • 节流 Ajax:输入 hellooooooooo... 此时用户一直没停手,但控制台会每隔 500ms 规律地打印一次。

结语

  • 防抖 是为了防止“误触”和“过度敏感”,它关注结果。
  • 节流 是为了“降频”和“减轻压力”,它关注过程。

掌握了这两个高阶函数,你的代码性能和用户体验都将上一个台阶。下次面试官问起闭包的应用场景,别再只说“变量不销毁”了,直接把这两个“守门员”甩出来!