手撕代码系列(三):一石二鸟探寻防抖和节流的多种实现思路

1,083 阅读4分钟

前言

防抖节流是前端性能优化的两大利器,核心思想都是限制高频行为

它们的区别在于:

  • 防抖,意在当外界不再变化时,再去做响应。

  • 节流,意在不管外界如何变化,始终保持着自己的响应频率。

应用场景:

  • 防抖:用户疯狂点击提交按钮,用户每次点击提交都会向服务器发出请求,如果不做限制那么会无意义地消耗服务器资源。因此,我们利用防抖的思想,做到当用户停止点击按钮后,再去放出请求。防抖演示图如下: 防抖演示图
  • 节流:当用户在更改浏览器窗体大小时,会触发resize事件。内置的resize事件响应频率太高,不仅对用户体验没有提升,反而还消耗了大量主线程的资源,导致卡顿。因此我们更希望他能降低响应的频率,那么节流的思想就很好的对应了这个需求。节流演示图如下(这里用scroll来做实验): 节流演示图

定时器版本

防抖和节流的定时器都只需要简单的设置和清除一下定时器,首先我们先写防抖。

防抖代码如下:

function debounce(fn, delay) {
    let timer = null;
    return function(...args) {
        if(timer) clearTimeout(timer);
        timer = setTimeout(function() {
            fn(...args);
        },delay)
    }
}

在此基础上,节流仅需改动两行代码:

function throttle(fn, delay) {
    let timer = null;
    return function(...args) {
        // 第一处改动
        if(timer) return;
        timer = setTimeout(function(){
            // 第二处改动
            timer = null;
            fn(...args);
        },delay)
    }
}

补充说明:不得不提一嘴,上述代码如果fn内部用到了this,那么this指向哪呢?在非严格模式下是全局对象(浏览器环境为window)。在严格模式下则是undefined。所以,如果fn用到了this,那么它的表现可能会不符合你的预期。为此,我们要么借助bindapplycall去显式绑定this,要么用箭头函数的形式去声明fn,从而让this的指向更符合我们的直觉。

用 rAF 去做节流

rAF,即requestAnimationFrame,因篇幅受限本文不再对该函数进行分析。大家可以参考昊神的文章

在这里,我们简单地将rAF看成一个间隔是 16.7ms 的定时器(当然它们不完全等价),我们把最小间隔时间固定为了约 16.7ms,据此来实现 N*16.7ms 的效果。相比于一般定时器而言,rAF可是要“守时”的多。

这里只是出于研究的目的去拓宽思路,大家了解下即可,本人也没有去验证其可靠性

function throttle(fn,delay) {
    // 估算一下最大计次数
    let maxCount = delay/(1000/60),
        count = 0;
    return function(...args) {
        requestAnimationFrame(function() {
          count++;
          if(count > maxCount) {
              fn(...args);
              count = 0;
          }
        });
    }
}

补充说明:这段代码存在一个问题,它无法保证最后一次回调函数一定被执行。即若持续触发resize3 秒,而节流要求的间隔时间为 2 秒,那么我们预期的效果是在 4 秒左右会响应两次,但上述代码只会响应一次,最后一次将被丢失。 下面的动图演示了这个 BUG。 rAF

用 rIC 去做防抖

rIC,即requestIdleCallback大家同样可以参考昊神的文章

rIC的空闲回调可能执行的时机有两种,要么是每帧之间的间隔时间,要么是用户停止交互后可以匀出个约 50ms 的时间。我们做防抖可以利用后者。

从防抖的目标出发,当用户不再频繁做点击交互时,我们再去进行响应。那么如何能够获知用户不再进行操作呢?就是通过判断空闲回调函数的入参IdleDeadline.timeRemaining()的值是否介于 16.7ms 到 50ms 之间,虽然它并不完全可靠。

注意:这里只是出于研究的目的去拓宽思路,大家了解下即可,本人也没有去验证其可靠性

function debounce(fn) {
    let lock = false;
    return function(...args) {
        // 上锁防止重复调用
        if (lock) return;
        const run = function() {
            requestIdleCallback(function(deadline) {
                // 打印一下空闲时间
                console.log(deadline.timeRemaining());
                lock = true;
                // 判断空闲时间
                if (deadline.timeRemaining() > 1000 / 60) {
                    fn(...args);
                    // 解锁
                    lock = false;
                } else {
                    // 递归
                    run();
                }
            });
        }
        run();
    }
}

下面让我们来演示一下。
rIC 从演示图中可以看出,平均点击2,3次将触发一次响应,离我们真正业务要求还有一定的距离。

写在最后

本文给出了常用的实现防抖和节流的方法,并借助rAFrIC做了简单实验。建议大家可以看看lodash是怎么封装和实现的防抖节流的。对于新手来说,也可以从防抖和节流的实现去体验闭包的妙用!

关于我

喜欢聊天、喜欢分享、喜欢前沿的22届小菜鸡,初来乍到希望得到各位大佬的关注。能有实习/校招机会就更好啦!
个人公众号:鼠子的前端CodeLife