『前端大白话』之“防抖和节流”

1,279 阅读6分钟

防抖和节流

面试经常被问到防抖和节流的问题,其实很多概念都只是讲的很 “高大上”(个人愚见),以至于听起来就很懵。 今天就用大白话和大家聊聊「防抖和节流」到底是个啥。

为啥会出现这么两个概念?

首先,防抖和节流当然不是凭空出现的,它是来解决问题的。解决啥问题呢?

比如网页上有一个下载按钮,这个按钮是下载图片用的,点一下就可以把这张图直接下载到你的电脑里。那么无论你是成心之过还是无意之举,在一秒钟内你用你单身20年无与伦比的手速抱着把鼠标点坏的决心点了 500 次,然后你的电脑里就有了 500 张相同的图片。

怎么样,感觉还爽吗?23333……

这种情况显然不合理,我所说的不合理不是说你没有那么快的手速,而是说程序允许你在短时间内可以连续的触发同一个事件,这件事它不合理。因为且不说你不需要 500 张一模一样的图,短时间内这么做对程序和服务器的带宽都是一种压力。

于是乎,「防抖和节流」它就 lei 了……

它们就是来解决这个问题滴~(当然这种情况很多,不光是点击按钮。)

什么是防抖

防抖,英文:debounce。

非立即执行版

触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
也就是说当一次事件发生后,事件处理器要等一定时间,如果这段时间过去后再也没有事件发生,就处理最后一次发生的事件。

啥意思?

假设你点了一个按钮(一次事件),我现在设定你点了按钮后必须过 1 秒才能生效(执行事件)。结果还差 0.01 秒就到达指定时间,也就是过了 0.99 秒,这时候你按捺不住自己内心的躁动与渴望又点了一次按钮(又发生了一次事件),那么恭喜你,之前的等待作废,需要重新再等待 1 秒(指定时间)。直到...直到你点了一次按钮之后 1 秒都没有动作,然后才会生效。

立即执行版

触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数。如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

啥意思?

emmm...这么聪明的你应该能举一反三了吧!

其实就是点了按钮会马上执行一次。然后...然后又和非立即执行版一个“德行”了...
你再点,他又得重新等 1 秒,你一直点,它就一直不执行,直到你点完之后一秒内不点了才会执行。

话不多说

非立即执行版:

function debounce1(fn, delay) {
    //注意下面不要用箭头函数,注意this和arguments的指向
    let timer = null; //通过闭包保存一个标记
    return function () {
        if (timer) {
            clearTimeout(timer);
        }
        //若有timer,就清除timer,所以会重新执行下面的定时器,重新定时。
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, delay);
    }
}
//对比两种写法
function debounce2(fn, delay) {
    let timer = null;
    //注意下面不要用箭头函数,注意this和arguments的指向
    return function (...args) {
        let context = this;
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function () {
            fn.apply(context, args);
        }, delay);
    }
}

立即执行版:

function debounce_(fn, delay, immediate) {
    let timer = null;
    return function () {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            immediate = true;
        }, delay);
        //以上代码每次点击都会执行。
        //若定时器没有结束,则immediate为false,下面代码else永远不会执行。
        //而return false之后若再重新执行此函数,又会清除掉定时器,再重新开一个timer,则会重新计算时间。
        if (!immediate) {
            return false;
        } else {
            fn.apply(this, arguments);
            immediate = false;
        }
    }
}

合并版:

//最终合并完整版
function debounce(fn, delay, immediate) {
    let timer = null;
    let canRun = immediate;
    return function () {
        if (timer) {
            clearTimeout(timer);
        }
        if (immediate) {
            timer = setTimeout(() => {
                canRun = true;
            }, delay);
            //用canRun代替了immediate
            if (!canRun) {
                return false;
            } else {
                fn.apply(this, arguments);
                canRun = false;
            }
        } else {
            timer = setTimeout(() => {
                fn.apply(this, arguments);
            },delay);
        }
    }
}

什么是节流

节流,英文:throttle。

节流就是类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活,喜欢玩 moba 类游戏的同学厉害了,这个东西就类似于技能冷却(技能 cd)。

大白话

实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1 秒,那么 1 秒内这个函数一定不会被调用两次。

说白了就是一个按钮你一直不停的点点点点点,其实它一秒就能执行一次。换做讲防抖开始时候那个极不贴切的下载图片的例子就是,即便你十秒内点了一万次,其实你也就只能一秒下载一张图,一共十张而已。

上代码

两种思路

第一种思路:

判断当前时间和上一次执行时间的时间差,如果这个时间差大于阈值,才让其执行。

意思就是当你点击按钮的时候,我判断一下你现在是几点几分几秒点击的按钮,然后减去上一次你点击按钮的时间,看看这个时间差有没有到 1 秒钟。没有上次点击咋办?凉拌(白眼)!

function throttle_(fn, time) {
    let pre = 0;
    return function (...args) {
        if (Date.now() - pre > time) {
            fn.apply(this, args);
            pre = Date.now();
        }
    }
}

第二种思路:

设置一个开关阀门(标记)。如果你点击了一次按钮,就把阀门关了,等 1 秒后再开阀门,也就是技能冷却的思路。

function throttle(fn, delay) {
    let canRun = true,
    timer = null; // 通过闭包保存标记
    return function () {
        // 在函数开头判断标记是否为true,不为true则return
        if (!canRun) {
            return false;
        }
        //定时器的作用:当delay秒过后,canRun才能变为true,代码才能执行到这里,才能继续往下执行
        canRun = false; // 立即设置为false
        clearTimeout(timer);
        timer = setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
            fn.apply(this, arguments);
            // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远false,在开头被return掉
            canRun = true;
        }, delay);
    };
}

2023 简洁版


    /**
    * @name 防抖 适用于input输入框不断输入发送请求等场景
    * @param fn 需要防抖的函数
    * @param daley 需要延迟的时间(毫秒)
    * @return 返回设置好延迟的防抖的函数
    */
    function debounce(fn, delay) {
        let timer = null;
        return function () {
            if (timer) clearTimeout(timer);
            timer = setTimeout(() => {
                fn.call(this, arguments);
                timer = null;
            }, delay);
        };
    }
    
    /**
    * @name 节流 适用于drag,scroll等场景
    * @param fn 需要节流的函数
    * @param daley 需要间隔的时间(毫秒)
    * @return 返回设置好间隔的节流的函数
    */
    function throttle(fn, delay) {
        let timer = null;
        return function () {
            if (timer) return;
            timer = setTimeout(() => {
            fn.apply(this, arguments);
                timer = null;
            }, delay);
        };
    }
    

写在最后

若文中有什么错误的地方,欢迎批评指导。若您有更好的方案或者写法,也欢迎留言。