throttle&debounce

214 阅读7分钟

1.理解

throttle:限定函数在一定间隔时间内只执行一次。

debounce:某个函数在某段时间内无论被触发多少次,都只执行最后一次。

这样看这两种概念比较迷惑,我们通过一个例子来理解这两个概念。

节流例子:

比如过安检,在开始安检后,在一定时间(10s)内只允许一个乘客过安检,以配合安检人员完成工作。throttle的点在于“一定时间内,限制动作只执行一次

防抖例子:

比如坐电梯的时候,如果电梯检测有人进来的,就会再重新等待10s,如果这10s内,电梯又检测到有人进来了,电梯将重新等待10s,直到10s过后没有人进来,电梯才会关闭开始运行。debounce的点在于“一个事件发生一定时间之后,才执行特定动作

然后再结合一张原理图来理解:


2.适用场景

直到节流和防抖的原理后,那么他们分别使用于什么样的场景呢?

这两种实现都是基于前端性能优化考虑的。比如说对于频繁操作或者频繁触发的事情,需要做一些限制,如果不做限制,有可能在1s内执行很多次,会消耗计算机的性能。

节流场景:

  • scroll滚动过程中需要监听滚动条的位置
  • 防止高频点击事件

防抖场景:

  • 搜索框输入。只需要用户最后一次输入完,再发送请求
  • 浏览器矿口大小改变,只需要调整之后再执行代码

当然适用场景还是需要结合实际需求来决定的。不过不管用哪一种,都是对性能的一种优化。



3.throttle的实现

实现方案

第一种时间戳来判断是否已到执行时间

缺点:
首次没有执行
事件触发结束后,无法响应回调(trailing:true无效)

var throttle = function(fn, delay = 1000) {    
    var pre = new Date();    
    return function() {        
        var now = new Date();        
        if (now - pre > delay) {            
            fn.apply(this, arguments);            
            pre = now;        
        }    
    };
};

var fn = () => {    console.log("函数被调用");};var throttleHandler = throttle(fn, 2000);var start = new Date();var interVal = setInterval(() => {    var end = new Date();    console.log("间隔时间", end - start);    throttleHandler();}, 1000);setTimeout(() => {    clearInterval(interVal);}, 7000);
//运行结果/*
间隔时间 1000
间隔时间 2001
函数被调用
间隔时间 3001
间隔时间 4001
函数被调用
间隔时间 5000
间隔时间 6001
函数被调用
间隔时间 7000
*/


第二种利用定时器来实现

缺点:
首次没有执行
事件触发结束后,必然会响应回调(trailing:false无效)

var throttle = function(fn, delay) {    
    var timer = null;    
    return function() {        
        if (timer) return;        
        timer = setTimeout(() => {            
            fn.apply(this.arguments);            
            timer = null;        
        }, delay);    
    };
};

用一个例子结合图片解析这两种实现方式的缺点:

var throttleHandler = throttle(fn, 2000);
var start = new Date();
var interVal = setInterval(() => {    
    var end = new Date();    
    console.log("间隔时间", end - start);    
    throttleHandler();    
    start = end;
}, 1000);
setTimeout(() => {    
    clearInterval(interVal);
}, 7000);
//运行结果
/*
间隔时间 1001
间隔时间 2001
间隔时间 3001
函数被调用
间隔时间 4001
间隔时间 5001
间隔时间 6000
函数被调用
间隔时间 7000
函数被调用
*/

根据执行结果我们可以分析到:

setinterval是从1000毫秒开始的,然后隔2000毫秒(也就是3000毫秒),执行一次回调。然后4000毫秒的时候又执行了setinterval重新设置settimeout为2000,也就是6000毫秒之后函数再次被触发。但是7000毫秒的时候,回调被中断了,但是最后一次还是会执行。


结合上面两种实现方式来看,利用时间戳应该是更合理的,但是利用时间戳的话,当事件结束后,是没有办法响应的,即不适合trailing=true的情况。

underscore则是结合了这两种方式,实现了更多的可配置项。

underscore简易版本实现方法(结合时间戳和settimeout)

var throttle = function(fn, delay, option = {}) {   
//pre为0表示第一次需要执行,因为now-pre>delay肯定满足,其他情况表示不需要执行     
    var pre = 0;
    var timer = null;    
    var isLeading = option.leading === false ? false : true;    
    var isTrailing = option.trailing === false ? false : true;    
    return function(...args) {        
        let now = +new Date();        
        //第一次不需要执行的情况 pre = now        
        if (!pre && !isLeading) {            
            pre = now;        
        }        
        //第一次需要执行 now - pre > delay        
        //或者间隔事件超过了delay        
        if (now - pre >= delay) {            
            if (timer) {                
                clearTimeout(timer);                
                timer = null;            
            }            
            pre = now;            
            fn.apply(this, args);        
        } else if (isTrailing && !timer) {            
            timer = setTimeout(() => {                
                pre = isLeading ? +new Date() : 0;                
                fn.apply(this, args);                
                timer = null;            
            }, delay - (now - pre))       
         }    
    }
};

默认情况:

var fn = () => {    
console.log("函数被调用");};
var throttleHandler = throttle(fn, 2000);
var start = new Date();
var interVal = setInterval(() => {    
    var end = new Date();    
    console.log("间隔时间", end - start);    
    throttleHandler();
}, 1000);
setTimeout(() => {    
    clearInterval(interVal);
}, 8000);
//执行结果
/*
间隔时间 1001
函数被调用
间隔时间 2001
间隔时间 3001
函数被调用
间隔时间 4002
间隔时间 5001
函数被调用
间隔时间 6001
间隔时间 7001
函数被调用
间隔时间 8003
函数被调用
*/

leading:false测试代码

var fn = () => {    
console.log("函数被调用");};
var throttleHandler = throttle(fn, 2000, { leading: false });
var start = new Date();
var interVal = setInterval(() => {    
    var end = new Date();    
    console.log("间隔时间", end - start);    
    throttleHandler();
}, 1000);
setTimeout(() => {    
    clearInterval(interVal);
}, 8000);
//执行结果
/*
间隔时间 1001
间隔时间 2001
间隔时间 3001
函数被调用
间隔时间 4001
间隔时间 5002
函数被调用
间隔时间6000
间隔时间 7001
函数被调用
间隔时间 8001
函数被调用
*/

trailing:false测试代码

var fn = () => {    
    console.log("函数被调用");
};
var throttleHandler = throttle(fn, 2000, { trailing: false });
var start = new Date();
var interVal = setInterval(() => {    
    var end = new Date();    
    console.log("间隔时间", end - start);    
    throttleHandler();
}, 1000);
setTimeout(() => {    
    clearInterval(interVal);
}, 8000);
//执行结果
/*
间隔时间 1000
函数被调用
间隔时间 2000
间隔时间 3000
函数被调用
间隔时间 4000
间隔时间 5000
函数被调用
间隔时间 6001
间隔时间 7000
函数被调用
间隔时间 8000
*/


debounce实现

debounce就相对于简单一点。实现原理也是用settimeout,函数执行一次时设定一个定时器,之后调用的时候,发现已经有定时器就了,就清空当前定时器,并重新设置一个定时器。如果存在没有被清空的定时器,当定时器计时结束后触发函数

定时器实现

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

测试代码

var fn = () => {    
    console.log("函数被调用");
};
var debounceHandler = debounce(fn, 2000);
var start = new Date();
var interVal = setInterval(() => {    
    var end = new Date();    
    console.log("间隔时间", end - start);    
    debounceHandler();
    }, 1000);
setTimeout(() => {    
    clearInterval(interVal);
}, 3000);
//执行结果
间隔时间 1001
间隔时间 2001
间隔时间 3001
函数被调用

首次被执行的实现

var debounce = function(fn, delay = 500, immediate) {    
let timer = null;    
    return function(...args) {        
        if (timer) {            
            clearTimeout(timer);        
        }        
        if (immediate && !timer) {            
            fn.apply(this, args);        
        }        
        timer = setTimeout(() => {            
            fn.apply(this, args);        
        }, delay);    
    }
};

测试代码

var fn = () => {    
    console.log("函数被调用");
};
var debounceHandler = debounce(fn, 2000,true);
var start = new Date();
var interVal = setInterval(() => {    
    var end = new Date();    
    console.log("间隔时间", end - start);    
    debounceHandler();
}, 1000);
setTimeout(() => {    
    clearInterval(interVal);
}, 3000);
//执行结果

间隔时间 1001
函数被调用
间隔时间 2000
间隔时间 3000
函数被调用