面试常见题之防抖节流

39 阅读2分钟

是什么

防抖和节流本质上都是前端对过高频率执行的限制。

防抖 : 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

节流 : 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

理解:

防抖是公交车,等最后一个人上来之后再多等三分钟,每次有人上来就重新计时。

节流是地铁,五分钟内就只有这一班。

防抖代码实现

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

let timer;
function debounce(fn, delay) {
    clearTimeout(timer);
    timer = setTimeout(function(){
        fn();
    }, delay);
}

在上面的代码中,会出现一个问题,timer被放到了全局作用域中,我们应该用闭包将这个变量包裹在防抖函数内部。

function debounce(fn, delay) {
    let timer; // 维护一个 timer
    return function (...args) {
        let _this = this; // 缓存this指向
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function () {
            fn.apply(_this, args); 
        }, delay);
    };
}

节流代码实现

在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

function throttle(fn, delay) {
    let timer;
    return function (...args) {
        let _this = this; // 缓存this指向
        if (timer) return;
        timer = setTimeout(function () {
            fn.apply(_this, args);
            timer = null; 
        }, delay)
    }
}

这个函数中 if (timer) return; 也是就是说如果当前存在一个计时器,后续的逻辑就不执行了。

或者用时间戳来实现:

function throttle(fn, delay) {
    let previous = 0;
    return function(...args) {
        let now = new Date();
        if(now - previous > delay) {
            fn.apply(this, args);
            previous = now;
        }
    }
}

这里其实这两种方案都符合预期,即在一个单位时间内,只能触发一次函数。 但在实际应用的场景下,我希望执行的最后一次是要触发的,例如类似百度的输入框。 我想要随着输入框变化进行实时搜索,假设我们需要在这里加一个节流,我肯定希望用我最后输入的结果去请求。 但是我们这种写法如果最后一次变更和倒数第二次间隔不够的话,最后一次是不会触发的。

所以我们对他做一个优化,将上面两种方式结合起来就能实现:

function throttle(fn, delay) {
    let previous = 0;
    let timer = null
    return function() {
      let _this = this; // 缓存this指向
      let now = new Date();
      if (new Date() - previous > delay) {
            clearTimeout(timer);
            timer = null;
            previous = new Date();
            fn.apply(_this, args)
        } else {
            timer = setTimeout(() => {
                fn.apply(_this, args)
            }, delay)
        }
    }
}