JavaScript防抖与节流的具体实现及使用场景

前言

最近遇到注册登陆按钮需要添加防抖功能,在进行了简单了解和实践之后,将其整理记录下来。
附:个人目前对防抖节流函数的最佳(更准确清晰)实现:
防抖函数及其使用示例
节流函数及其使用示例

一、什么是防抖和节流?

防抖和节流是针对响应跟不上触发频率这类问题的两种解决方案。针对此类快速连续触发和不可控的高频触发问题,debounce 和 throttle 给出了两种解决策略。在阅读本篇文章之前需要对闭包和超时调用(setTimeout)的相关知识有一定的了解。

二、防抖

简单介绍

防抖分为两种,一种是延迟执行的,叫做延迟防抖;另外一种是立即执行的,叫做前缘防抖。

注:(图解及部分代码思路参考自文末参考文档,个人觉得此图解较为清晰,另外,非常感谢前人留下的宝贵经验)

图解

延迟防抖图解:
在这里插入图片描述
前缘防抖图解:
在这里插入图片描述

代码如下(示例):
// 真正需要执行的函数,事件处理之类的
fn=()=>{
	console.log('发起网络请求');
}

1.一个简单的防抖函数

// 延迟防抖基础版
  debounceOne=(fn,delay)=>{  // 它只执行一次
    let timer = null;
    console.log('延迟防抖函数基础版')
    return(
      ()=>{ // 匿名函数(闭包)
        clearTimeout(timer);
        timer = setTimeout(
          ()=>{ //这里用箭头函数
            fn(); // 调用回调函数后将timer置空
            timer=null;
          },delay
        );
      }
    )
  }

下面的代码中注意此处,this.debounceOne(this.func,2000)表示立即调用执行,一加载到debounceOne()这里就执行,然后当我们点击按钮的时候,执行的就是返回来的闭包函数了,而写在debounceOne函数中的非返回部分只在最初立即执行时执行一次,我们可以在那部分进行一些初始化工作。

<Button type='primary' onClick={this.debounceOne(this.fn,2000)} style={{marginBottom:'10px',display:'block'}}>点击发起请求(延迟防抖基础版)</Button>

2.在上面的基础上进行优化

// 延迟防抖优化版,不用多次清除,创建定时器,而是改变开始时间,在点击按钮到fn函数执行的期间内最多只创建了两次定时器,提高了性能
  debounceTwo = (fn,delay)=>{
    let timer = null;
    let triggerTime;
    console.log('延迟防抖函数优化版')
    let run =(wait)=>{
      timer = setTimeout(()=>{
        let executeTime = (new Date()).getTime();
        let alreadyWait = executeTime - triggerTime; // 计算开始时间戳(点击按钮时会被修改)与执行时间戳的差值
        if(alreadyWait < wait){
          run(delay-alreadyWait); //此处用delay不用wait
        }else {
          fn();
          timer = null;
        }
      },wait)
    }
    
    return (()=>{
      triggerTime = (new Date()).getTime(); // 点击按钮记录(修改)开始时间戳
      if(!timer){
        run(delay);
      }
    })
  }
<Button type='primary' onClick={this.debounceTwo(this.fn,2000)} style={{marginBottom:'10px',display:'block'}}>点击发起请求(延迟防抖优化版)</Button>

3.再向其中添加一个参数immediate,值为true时表示采用前缘防抖,该值也可由外部传入进行更改,具体代码有一些不同,下面有标注

  // 前缘防抖优化版
  debounceThree=(fn,delay,immediate=true)=>{
    let timer = null;
    let triggerTime;
    console.log('前缘防抖函数优化版'); // 只执行一次
    let run=(wait)=>{
      timer= setTimeout(()=>{
        let executeTime = (new Date()).getTime();
        let alreadyWait = executeTime-triggerTime; // 计算开始时间戳(点击时会被修改)与执行时间戳的差值
        if(alreadyWait < wait){
          run(delay-alreadyWait); //此处用delay
        }else {
          if(!immediate){ // 相对于延迟版本的变化
            fn();
          }
          // fn();

          timer = null;
        }
      },wait);
    }

    return (()=>{
      triggerTime = (new Date()).getTime(); // 点击记录(修改)开始时间戳
      if(!timer){
        if(immediate){ // 相对于延迟版本增加的部分
          fn();
        }
        run(delay);
      }
    })
  }
<Button type='primary' onClick={this.debounceThree(this.fn,2000)} style={{marginBottom:'10px',display:'block'}}>点击发起请求(前缘防抖优化版)</Button>

4.看到这里的话,基本上就可以理解防抖函数该怎么使用了。但是此时fn()函数是没有给它传入参数的,如果我们需要在fn()函数使用外部传入的参数,要怎样将参数传给fn()呢?来看接下来的拓展。

  //真正需要执行的函数,事件处理之类的,带arguments版本,使用普通函数
  funcWithArguments(){
    console.log('发起网络请求');
    console.log('传入参数:',arguments);

    // .......将arguments用于后续代码中
  }

拓展1:

// 延迟防抖优化带参数版,使用arguments,此处使用function 而不使用箭头函数
  debounceTwoWithArguments (fn,delay){
    let timer = null;
    let triggerTime;
    console.log('延迟防抖函数高性能带参数版');  // 只执行一次
    let run=function (wait){ //此处为了接受点击闭包时的arguments,用普通函数
      timer = setTimeout(()=>{
        let executeTime = (new Date()).getTime();
        let alreadyWait = executeTime - triggerTime; // 计算开始时间戳(点击时会被修改)与执行时间戳的差值
        if(alreadyWait < wait){
          run(delay-alreadyWait); //此处用delay更准确,不用wait
        }else {
          console.log('参数:',arguments);
          fn.apply(this,arguments);

          // fn();
          timer = null;
        }
      },wait)
    }

    return (function() { // 此处为了使用arguments,用普通函数
      triggerTime = (new Date()).getTime(); // 点击记录(修改)开始时间戳
      if(!timer){
        console.log(arguments); //点击时闭包函数的arguments
        run(delay,arguments); // 将闭包函数的arguments作为参数传入run,以供在run函数内的fn函数使用
      }
    })
  }
<Button type='primary' onClick={this.debounceTwoWithArguments(this.funcWithArguments,2000)} style={{marginBottom:'10px',display:'block'}}>点击发起请求(延迟防抖带参数优化版)</Button>

拓展2:

  // 前缘防抖优化带参数版,使用arguments,注意此时多处使用function 而不使用箭头函数
  debounceThreeWithArguments(fn,delay,immediate=true){
    let timer = null;
    let triggerTime;
    console.log('前缘防抖函数带参数优化版')
    let run=(wait)=>{
      timer= setTimeout(()=>{
        let executeTime = (new Date()).getTime();
        let alreadyWait = executeTime-triggerTime; // 计算开始时间戳(点击时会被修改)与执行时间戳的差值
        if(alreadyWait < wait){
          run(delay-alreadyWait); //此处用delay更准确,不用wait
        }else {
          if(!immediate){ // 相对于延迟版本的变化
            fn();
          }
          // fn();

          timer = null;
        }
      },wait);
    }

    return (function() {
      triggerTime = (new Date()).getTime(); // 点击记录(修改)开始时间戳
      if(!timer){
        if(immediate){ // 相对于延迟版本的增加
          // fn();

          //相比前缘优化版变动
          console.log('回调参数',arguments); // 获取到当我们点击按钮时,传入闭包函数的参数列表
          fn.apply(this,arguments);
        }

        run(delay);
      }
    })
  }
<Button type='primary' onClick={this.debounceThreeWithArguments(this.funcWithArguments,2000)} style={{marginBottom:'10px',display:'block'}}>点击发起请求(前缘防抖带参数优化版)</Button>

关于这里为什么使用function,原因在于箭头函数没有arguments属性,在需要传递未知数量的参数时用arguments会更方便,在使用时要注意this指向。另外一个地方是apply,一般用法为fn.apply(this,arguments),有兴趣的小伙伴可以去了解一下,这里不做详述。

二、节流

简单介绍

节流主要用于控制某项操作,在规定的时间(delay)内,只能执行一次,当经过了该时间周期后,才能再次执行。与防抖类似,节流也分为立即执行和非立即执行两种。

图解

延迟节流图解:
在这里插入图片描述
前缘节流图解:
在这里插入图片描述

代码如下(示例):
// 真正需要执行的函数,事件处理之类的
fn=()=>{
	console.log('发起网络请求');
}

1.节流函数,可选是否立即执行

//immediate=true表示前缘节流,否则为延迟节流
  throttle=(fn,delay,immediate=true)=>{
    let timer = null;
    return(
      ()=>{
        if(!timer){
          if(immediate){
            fn();
          }
          timer=setTimeout(()=>{
            if(!immediate){
              fn();
            }
            timer = null;
          },delay)
        }
      }
    )
  }
//一个简单的节流函数就完成了

下面的代码中注意此处,this.throttle(this.func,2000)表示立即调用执行,一加载到throttle()这里就执行,然后当我们点击按钮的时候,执行的就是返回来的闭包函数了,而写在throttle函数中的非返回部分只在最初立即执行时执行一次,我们可以在那部分进行一些初始化工作。

<Button type='primary' onClick={this.throttle(this.func,2000)} style={{marginBottom:'10px',display:'block'}}>点击发起请求(前缘节流版)</Button>

2.在上面的基础上进行拓展

// 真正需要执行的函数,事件处理之类的
funcWithArguments (){
    console.log('发起网络请求');
    console.log('参数arguments:',arguments);
  }
//immediate=true表示前缘节流,否则为延迟节流,带参数Arguments版
  throttleWithArguments (fn,delay,immediate=true){ // 使用普通函数
    let timer = null;
    return(
      function(){
        if(!timer){
          console.log(arguments); // 能够访问到点击后调用闭包函数时的arguments,该arguments可用于fn函数
          if(immediate){
            // fn();
            fn.apply(this,arguments); // arguments的使用,可将其传给fn函数
          }
          timer=setTimeout(function(){ // 此处需用普通函数,否则下面apply那里的this会有误
            if(!immediate){
              // fn();
              fn.apply(this,arguments); // arguments的使用,可将其传给fn函数
            }
            timer = null;
          },delay)
        }
      }
    )
  }
<Button type='primary' onClick={this.throttleWithArguments(this.funcWithArguments,2000)} style={{marginBottom:'10px',display:'block'}}>点击发起请求(前缘节流带参数版)</Button>

三.防抖与节流的使用场景

函数节流与函数防抖都是为了限制函数的执行频率,防止函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象,但是需要根据实际场景需求采用不同的防抖和节流方式。
防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误触发多次造成资源浪费。如果事件是高频触发且有一定停顿,可用防抖进行处理,如用户在短时间内多次点击登陆,搜索框根据输入的一部分值进行联想搜索(y也可以使用节流),短信验证码,resize等。
节流:控制流量,单位时间内事件只能触发一次,如scroll 事件,mouseover事件,播放事件等。
当事件可能高频触发且只需要执行一次时可选用防抖;当事件可能高频触发,需要执行多次且触发相对平滑时最好使用节流。


总结

以上就是我对函数防抖与节流的理解,如有不同观点欢迎评论区留言交流,参考文档如下。
参考文档1
参考文档2
参考文档3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值