JS中的节流和去抖动

首先要明白 节流 Throttle 和 去抖动 Debounce 两者是有区别的,很多人一开始都会搞混。
先讲讲去抖动 Debounce

Debounce

为什么要去抖动?

我们知道 浏览器有一些原生事件,比如 resize scroll keyup keydown 这些事件的回调函数,当他们触发的时候,并不是想象中的只触发一次,而是几次甚至几十次,如果当你的这些事件回调函数中有一些复杂的运算或者dom操作,低配浏览器很容易出现假死的状态。

去抖动Debounce实现的效果是:以scroll来举例,当scroll回调在指定的时间n毫秒内还会触发,此次回调方法不执行,继续等待n毫秒,直到n毫秒之后此方法不再触发,执行这个方法。简单来说就是:把在指定时间内可能会多次执行的方法打包成一次

window.debounce = function(fun,dely){  //fun 需要去抖动的方法,dely 指定的延迟时间
    var timer = null;  // 用闭包维护一个timer 做为定时器标识
    return function(){
        var context = this;  // 调用debounce的时候 保存执行上下文
        var args = arguments;  
        clearTimeout(timer);
        timer = setTimeout(function() {
            fun.apply(context , args);
         }, delay); // 设定定时器 判断是否已经触发 ,如果触发则重新计时 等待dely毫秒再执行
    }
}

此时如果调用

foo = function(){
    console.log('scroll work')
}
dom.addEventListener('scroll', debounce(foo, 2000)); // 当dom连续触发scroll 时 回调函数只会在两秒后执行一次 

但是这种写法有一个明显的缺陷,就是当用户触发的第一时间方法是不会调用,所以上升级版

window.debounce = function(fun,dely){ 
    var timer = null; 
    return function(){
        var context = this;  
        var args = arguments;  
        if(timer) { clearTimeout(timer) }; // 看似多余的 但是是必须的 读者可以自己思考为什么需要这么处理
        var doNow = !timer; // 判断是否有定时器,如果有,就dely后清除timer,否则立即执行;
        timer = setTimeou(function(){
            timer = null ;
        },dely)
        if(doNow){
            fun.apply(context, args);
        }
    }
}

现在的效果是,你滚动的第一时间会触发回调,然后你要是连续再触发,在dely秒之内是不会触发的,只有等dely毫秒后 timer 清除了,再触发滚动才会调用回调。

想必两个版本的问题大家都看出来了,多多少少都是有点奇怪。接下来就是节流登场了

Throttle

节流函数是处理类似场景但抖动不适合的另一种解决方案,比如大型电商网站当用户滚动到页面底部的时候再发AJAX请求获取图片,实现图片懒加载,如果使用去抖动,不管方案一还是二,都会用种奇怪的体验,假设设置500ms的delay时间,使用方案一,效果则是,用户滚动了,500ms后发AJAX获取图片,再显示图片。期间500ms用户是只能看到图片缺失的。如果使用方案二,似乎是能实现需求,但是仔细想想,如果用户不是500ms滚动一次,而是玩命的在连续滚动,则AJAX只会触发一次,用户只能看到第一次滚动触发AJAX返回的图片,后面的则是图片缺失状态。

到这应该可以猜到节流实现的什么效果了。

节流函数允许一个函数在规定的时间内只执行一次。

它和防抖动最大的区别就是,节流函数不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。

主要有两种实现方法:

1.时间戳
2.定时器

时间戳实现:
window.throttle = function(fun,delay){
    var prev = Date.now(); // 闭包维护一个起始时间戳 
    return function(){
        var context = this;
        var args = arguments;
        var now = Date.now();  // 每次任务函数触发的时候获取时间戳
        if(now-prev>=delay){ // 判断当前时间与起始时间戳的间隔 大余delay则触发任务函数
            fun.apply(context,args);
            prev = Date.now(); // 关键是要更新闭包中的 起始时间戳
        }
    }
}

此时我们再测试

foo = function(){
    console.log('scroll work')
}
dom.addEventListener('scroll', throttle (foo, 1000)); // 当dom连续触发scroll 时 任务函数每隔1秒也会触发一次,当然眼尖朋友会发现有个小瑕疵
定时器实现:
var throttle = function(fun,delay){
    var timer = null; // 维护一个定时器
    return function(){
        var context = this;
        var args = arguments;
        if(!timer){ // 当任务函数触发了 , 判断定时器是否存在  不存在才执行任务函数
            timer = setTimeout(function(){ 
                fun.apply(context,args);
                timer = null;
            },delay);  // 当定时器不存在的时候 delay秒后才执行任务函数 并且清空定时器 接着下个轮回
        }
    }
}

当第一次触发事件时,肯定不会立即执行函数,而是在delay秒后才执行。
之后连续不断触发事件,也会每delay秒执行一次。
当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次函数。

可以综合使用时间戳与定时器,完成一个事件触发时立即执行,触发完毕还能执行一次的节流函数:

window.throttle = function(fun,delay){
    var timer = null;
    var startTime = Date.now();  

    return function(){
        var curTime = Date.now();
        var remaining = delay-(curTime-startTime);  // 计算出两次触发的时间间隔有没有大余delay 
        var context = this;
        var args = arguments;

        clearTimeout(timer);
        if(remaining<=0){ 
            func.apply(context,args);
            startTime = Date.now();  // 如果两次触发时间大余delay,则立马触发一次任务函数并且更新起始时间戳
        }else{
            timer = setTimeout(fun,remaining);  // 如果两次触发时间小于delay, 则改变定时器时间保证delay时间一定触发任务函数
        }
    }
}

总结

防止一个事件频繁触发回调函数的方式:

  • 防抖动debounce:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

  • 节流throttle :使得一定时间内只触发一次函数。
    它和防抖动最大的区别就是,节流函数不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而防抖动只是在最后一次事件后才触发一次函数。
    原理是通过判断是否到达一定时间来触发函数,若没到规定时间则使用计时器延后,而下一次事件则会重新设定计时器。

---end

你可能感兴趣的:(JS中的节流和去抖动)