javaScript的Debouncing(防抖)和 Throttling(节流)

“节流”和“防抖”都是用来提高用户体验,提高网站性能的手段,它们的技术手段都是“强制事件处理函数在特定的时间段内执行”。这样解释可能还是不够直观。举两个例子吧:

1: 比方说我们给document绑定了一个scroll的事件,scroll事件是每滑动一个px,scroll的处理函数就会被调用执行,如果在你的处理函数里面恰巧做了一个很花时间或者很花空间的事情,比方说复杂的运算啊,ajax请求啊,那这样页面就可能出现卡顿的情况。

2: 页面上有个地址的输入框,你希望根据客户的输入内容,去帮客户补全。假如说这个地址列表需要通过ajax请求来获取,那我们一定是希望在客户停止输入了之后再去请求ajax然后来补全,而不是客户一边输入就一直请求ajax。

针对上面举例的情况,其实运用节流和防抖都可以做到,只是它们之间又有一定的区别:

防抖:防抖是每次想要执行这个函数,都得先等上一段时间。
节流:节流是在一定的时间段内,函数最多可以被调用多少次。也可以理解为函数以一定的频率被调用。

语言总是苍白显得,直接来看代码的实现吧。我们先来实现一个防抖:

//实现防抖函数
function debouncing(fn, waitTime){
    let timer = undefined;
    return function(){
        let context = this;
        let args = arguments;
        clearTimeout(timer);
        timer = setTimeout(function(){
            fn.apply(context, args);
        }, waitTime)
    }
}
//scroll事件的处理函数
function scrollHandler(event){
    console.log(new Date(event.timeStamp));
}
//document的scroll事件上使用防抖函数
document.addEventListener('scroll', debouncing(scrollHandler,100), false);

实现防抖函数的核心就是每次事件被触发的时候,我们不是立即去调用相应的handler,而是每一次都重新设置一个timeout,等待一段时间,然后再执行我们的handler.

2: 现在来尝试实现一个节流函数:

function throttling(fn, intervalTime){
    let inInterval = false;
    return function(){
        let context = this;
        let args = arguments;
        if(!inInterval) {
            fn.apply(context, args);
            inInterval = true;
            setTimeout(function(){
                inInterval = false;
            }, intervalTime)
        }
        
    }
}

function scrollHandler(event){
    console.log(new Date(event.timeStamp));
}

document.addEventListener('scroll', throttling(scrollHandler,500), false);

节流的核心是管理一个布尔值开关变量(inInterval),以一定的频率切换它的true值和false值,事件处理函数只在这个开关变量值为某个特地值的时候才执行,以此来实现事件处理函数以一定的频率被调用。

节流函数它的实现有很多种,多种就在于控制这个开关变量的值的条件,会不一样。在上面的例子里,我通过setTimeout的方式,间断性的来改变inInterval的值。

现在来详细分析一下上面的实现:

1: 第一次scroll事件触发的时候,scrollHandler被立即执行。这个是我个人的一个考虑,希望对于第一次的事件触发能马上有一个回馈给客户。

2: 当第一次回调函数执行完了之后,我们马上把'inInterval=true', 假如这时候第二次scroll触发,代码执行到 if(!inInterval),此时条件表达式的值为false, 所以scrollHandler不会被立即执行。在这之后的第N次scroll事件触发的时候,inInterval都有可能还是是true,那么回调函数会一直不被执行。

3: 我们之前在把inInterval设置为true之后,同时设置了一个timeout,在经过一定的时间(intervalTime)之后,inInterval会被设置为false; 假如在这之后马上又触发了一次scroll事件,代码走到if(!inInterval),条件为true,scrollHandler就可以被执行了。

这一切看起来是这么完美地自圆其说,但是上面的代码存在一个问题,我们没有考虑到一个极端情况:假如我们最后一次的scroll事件,正好发生在这个循环时间内,那它就永远得不到执行了。这个可能会是一个隐藏的bug, 比方说你在进行一次拖拽事件,那目标元素可能永远都拖不到目的地。
所以我们要改一下代码,让最后一次事件的回调函数总是能被执行:

function throttling(fn, intervalTime){
    let inInterval = false;
    let lastTimer = undefined;
    let timer = undefined;
    return function(){
        let context = this;
        let args = arguments;
        if(!inInterval) {
            clearTimeout(lastTimer); //这行代码很重要
            fn.apply(context, args);
            inInterval = true;
            timer = setTimeout(function(){
                inInterval = false;
            }, intervalTime)
        }else{
            clearTimeout(lastTimer);
            lastTimer = setTimeout(function(){
                fn.apply(fn, args);
                inInterval = false;
            }, intervalTime);
        }
        
    }
}

function scrollHandler(event){
    console.log(new Date(event.timeStamp));
}

document.addEventListener('click', throttling(scrollHandler,1000), false);

上面的代码,要特别解释一下这一行代码:clearTimeout(lastTimer); //这行代码很重要

假如我们现在处理一个点击事件,如果我们不加这行代码的话,会出现先点击的click事件反而后执行的问题。比如;我们的intervalTime设置为10s, 然后我们分别在第0s, 第5秒,第12秒都进行一次点击,我们通过console.log(new Date(event.timeStamp)) 打印每一次事件发生时的时间, 我们会看到第5秒的那个click事件会比第12秒的那个click后输出,这就说明这里有问题。

所以我们要在if(!inInterval){}里面把lastTimer给清掉,也就是通过clearTimeout(lastTimer); 这行代码。

你可能感兴趣的