JS防抖与节流:详解与优化

137 阅读5分钟

JS防抖与节流

本文已发布在:JS防抖与节流 -Aiysosis Blog(aiysosis.ink)

前言

防抖和节流是前端编程中常见而重要的需求,如在注册时需要根据用户的输入值判断用户名是否已被占用、实时监听浏览器的滚动事件、用户疯狂点击按钮等等。如果不对请求的频率加以限制,那么会造成资源浪费和页面的不流畅,防抖和节流正是为了应对这样的情况而产生的。

基础知识

在防抖节流的实现中我们主要利用了JavaScript的闭包特性来维持一个单例且可访问的定时器,因此我们首先需要了解闭包相关的概念。这里推荐下这个系列的文章,讲解得还是比较系统的(不是广告哦🙂)。

深入理解javascript原型和闭包(14)——从【自由变量】到【作用域链】 - 王福朋 - 博客园 (cnblogs.com)

深入理解javascript原型和闭包(15)——闭包 - 王福朋 - 博客园 (cnblogs.com)

同时,函数的封装,以及定时器的特性,可能会导致this的指向错误,所以我们最好还要了解this指向相关的知识。简单来说,函数的this会指向调用它的对象,当我们在定时器中调用函数,由于浏览器的执行原理,this会指向全局对象window,这样就丢失了原有的指向性。

最后,我们还需要了解函数的arguments,这是函数的内置属性,它是一个类数组对象,记录了传入的参数,它的原型链上面没有数组的方法,但是我们可以这样操作:

function fn(){
    let arr = Array.prototype.slice.apply(arguments,[0]);
    //or arr = Array.prototype.slice.call(arguments,0)
    console.log(arr);
}
fn(1,'2',[3,4]);//output:[ 1, '2', [ 3, 4 ] ]

这个知识点在防抖与节流中用不上,只是我比较喜欢发散就写下来了,权当拓展和巩固。但是这里的applycall方法还是需要提前了解的,它们的主要用途就是绑定函数执行时的this

情景

在这里我们结合具体的情景进行代码的编写。场景很简单,就是一个计数器和一个按钮,用户点击按钮,计数器加1,以此模拟请求的发送。

<div class="wrapper">
    <div id="div">0</div>
    <button id="btn" >click me</button>
</div>
//js
let div = document.getElementById('div');
let btn = document.getElementById('btn');
​
function add(a,b,c){//参数没有实际作用
    console.log(this);
    console.log('args: ',a,b,c);
    let num = parseInt(div.innerHTML);
    num ++;
    div.innerHTML = ''+num;
}
​
btn.onclick = add(1,2,3);

当我们使用我们的麒麟臂疯狂点击按钮,可以看到数字在飞快地增长,这还只是一个用户发出的请求。因此我们需要通过代码把请求的频率控制在合理的范围内。

防抖

防抖的主要思想是:让用户的点击延迟一定时间(这里假设是1秒)生效,如果用户在一秒内又触发了点击事件,那就重新刷新等待的时间。只有当定时器正常走完,事件才被触发。简单说就是我知道你很急但是你先别急😅。

常见版本

function debounce(fn,wait){
    let timer = null;
    return function(){
        let ctx = this;//绑定this
        let args = arguments;//接受参数
        if(timer)clearTimeout(timer);
        timer = setTimeout(function(){
            fn.apply(ctx,args);//利用apply调用函数
        },wait)
    }
}

这样做有一个缺点,如果用户非常急,从头到尾一直在点,那么在高频点击期间一次请求都不会发出,我们可以稍作修改,至少让用户的第一次点击生效,不然除了用户,产品和客服可能都会很急😂。

function debounceImm(fn,wait){
    let timer = null;
    return function(){
        let _self = this;
        let args = arguments;
        clearTimeout(timer);
        if(timer === null){
            fn.apply(_self,args);
        }
        timer = setTimeout(function(){
            timer = null;
        },wait);
    }
}

全是坑

当我兴高采烈地准备使用的时候,掉坑里了。如果add没有参数,这两种写法都很完美,this指向了按钮,防抖功能也都能实现。

btn.onclick = debounce(add,1000);
btn.onclick = debounce(function(){...},1000);//里面是add的逻辑

但是当add有了参数,事情开始变得不对劲了。

btn.onclick = debounce(add(1,2,3),1000);//不行,add(1,2,3)立即执行了
btn.onclick = debounce(add,1000)(1,2,3);//同上,返回的函数被立即执行
btn.onclick = debounce(function(){add(1,2,3);},1000);//功能ok,但是this指向了window
btn.onclick = debounce(()=>add(1,2,3),1000);//同上,this指向了window

正解:

btn.onclick = debounce(function(){add.apply(this,[1,2,3])},1000);

这种调用方法实在是过于难受了,对于开发非常不友好。

改进

通过剩余参数和箭头函数,我们可以在简化代码的同时让调用方式更加友好。

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

立即执行:

function throttleImm(fn,wait,...args){
    let timer = null;
    return function(){
        if(timer === null){
            fn.apply(this,args);
            timer = setTimeout(()=>{
                timer = null;
            },wait)
        }
    }
}

调用:

//无参数
btn.onclick = debounce(add,1000);
btn.onclick = debounce(function(){/*...*/},1000);
//有参数
btn.onclick = debounce(add,1000,1,2,3);
btn.onclick = debounce(function(a,b,c){/*...*/},1000,1,2,3);

这样调用起来会方便很多,利用箭头函数简化了this的保存与再赋值,利用剩余参数统一了函数的调用方法。

节流

终于来到了节流部分,顺着刚才的情景,即使我们让用户的第一次操作生效,但如果用户很急,并且第一次操作又失败了,那么同样地,在疯狂点击的这段时间用户得不到任何响应,我们想要的是降低用户请求的频率,而不是让用户一次请求也发不出,因此节流应运而生。

节流的主要思想是:当用户疯狂点击时,可以以一个合适的频率不断发送请求(相当于为请求的频率设置了一个上限),我们同样可以基于定时器来实现。(这里的写法就直接应用箭头函数和剩余参数了)

function throttle(fn,...args){
    let timer = null;
    return function(){
        if(timer === null){//如果已经有计时器那么就不用管了
            timer = setTimeout(()=>{
                fn.apply(this,args)
                clearTimeout(timer);
                timer = null;
            },1000)
        }
    }
}

立即执行:

function throttleImm(fn,wait,...args){
    let timer = null;
    return function(){
        if(timer === null){
            fn.apply(this,args)//就是调换了一下顺序
            timer = setTimeout(()=>{
                timer = null;
            },wait)
        }
    }
}

调用:

//无参数
btn.onclick = throttle(add,1000);
btn.onclick = throttle(function(){/*...*/},1000);
//有参数
btn.onclick = throttle(add,1000,1,2,3);
btn.onclick = throttle(function(a,b,c){/*...*/},1000,1,2,3);