前端防抖与节流超详细讲解

简介: 前端防抖与节流超详细讲解

前言

防抖与节流通常作为项目优化的手段,一般都是为了防止用户在短时间内快而频地多次操作,触发动作执行。比如防止用户点击多次提交按钮,触发表单多次提交;防止用户拉动滚动条,多次触发加载更多等情况


防抖

什么是防抖

防抖,顾名思义,就是防止抖动,简而言之就是多次快速频繁地触发事件,也只会执行一次事件函数,但是要记住,需要加上一个时间限制(总不能上一次触发和下一次触发前后相隔半个钟,也才执行一次吧?这样就不是防抖了,而是bug了)。

所以,防抖最正确的解释应该是,为了防止快速且频繁的触发事件而导致多次执行事件函数,我们这多次触发的事件只执行一次事件函数。而对于如何定义“频繁”,我们可以设定一个间隔时间,如果两次事件触发的间隔时间低于设定的时间,则定义为频繁。这样的场景有很多,比如监听滚动、鼠标移动事件onmousemove、频繁点击表单的提交按钮等等


debounce的特点是当事件快速连续不断触发时,动作只会执行一次。分为两种,一种是延迟debounce,一种是前缘debounce。 延迟debounce,是在周期结束时执行,前缘debounce,是在周期开始时执行。但当触发有间断,且间断大于我们设定的间隔时间时,动作就会有多次执行。


延迟debounce策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。

image.png

前缘debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。


image.png

实现防抖函数

版本1: 周期内有新事件触发,就清除旧定时器,重置新定时器;这种方法,需要高频的创建定时器。

此方法属于延迟debounce,即周期结束时执行,而且这种方法是多次创建新的计时器去替换旧的计时器,意思就是周期会不断刷新替换,只在最后一次周期结束后,才会执行

importReactfrom'react'exportdefaultfunctionDebunceTest() {
// 创建debounce防抖函数constdebounce= (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为防抖的周期时长lettimeout: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例letcontext: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的thisletargs: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。// 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数)// 通过定时器延迟执行事件函数letrun= ()=>{
timeout=setTimeout(()=>{
// 通过 apply 修改func的this指向,并让func获取真正的事件函数(即防抖函数return出来的函数)的参数,之后执行funcfunc.apply(context,args);
      }, wait);
    }
// 清除定时器letclean= () => {
clearTimeout(timeout);
    }
returnfunction (this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错context=this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。// 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。args=arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。// 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了if (timeout) { // 说明当前仍然处在上一个计时周期过程中,并且又触发了相同事件。则取消上一个计时周期,重新创建周期,覆盖上一次的操作console.log('reset');
clean();  // 取消当前计时周期run();    // 重新创建周期,覆盖上一次的操作      } else { // 说明上一个计时周期已经结束,那么可以开启新的周期console.log('set');
run();
      }
    }
  }
// 要执行的事件函数consthandleClick= (e: any) => {
console.log('提交表单', e);
  }
return (
<divonClick={debounce(handleClick, 1500)}>提交表单</div>  )
}

版本2:周期内有新事件触发时,重置定时器开始时间戳,定时器执行时,判断开始时间戳,若开始时间戳被推后,重新设定延时定时器。并且增加是否立即执行选项(也就是前缘debounce或延迟debounce)

importReactfrom'react'exportdefaultfunctionDebunceTest() {
// 创建debounce防抖函数constdebounce= (func: any, wait: any, immediate: any) => { // func为原本事件触发后所需要执行的函数、wait为防抖的周期时长、immediate为是否先执行事件函数func再做防抖(true为前缘debounce,false为延迟debounce)lettimeout: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例letcontext: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的thisletargs: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。// 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数)lettimestamp: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来记录当前最新的定时器计时时间内最后一次事件触发的时间戳,因此借助闭包缓存当前最新的定时器计时时间内最后一次事件触发的时间戳letresult: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储func执行之后返回的结果,因此借助闭包缓存func执行之后返回的结果letlater=function() {
console.log(2111);
// 获取当前时间letnow= (newDate()).getTime();
// 获取当前定时器计时时间内最后一次事件触发至定时器计时结束的时间间隔letlast=now-timestamp;
// 如果当前间隔时间少于设定的时间wait且大于0就重新设置定时器://(比如wait传参是3000ms,而第一次计时周期内(3000ms内)最后一次触发事件发生在了2900ms,// 那么在经历完3000ms之后,执行定时器的回调函数,发现最后一次触发事件距离3000ms的计时结束的时间间隔才100ms,不足3000ms,// 这时候在执行定时器的回调时,不应该立马判定当前3000ms周期内的防抖结束,因为用户是有可能会在超过3000ms周期之后,比如超过3000ms周期100ms之后,还会触发事件,// 那么我们就应该继续执行防抖,阻止func的多次执行,所以需要再次启动定时器,再次启动的定时器的时间周期应该是 “wait-当前定时器计时时间内最后一次事件触发至定时器计时结束的时间间隔” ,// 而不是盲目的再次以wait为周期重新创建定时器(这样可以缩短启动下次防抖的时间,以及缩短非立即执行func的开始执行时间))if (last<wait&&last>=0) {
console.log(3221);
timeout=setTimeout(later, wait-last);
      } else {
console.log('用户停止触发事件且计时结束了');
// 清空timeout、context、args闭包变量,释放内存timeout=context=args=null;
// 如果不是立即执行,则执行funcif (!immediate) {
result=func.apply(context, args);
        }
      }
    };
returnfunction(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错context=this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。// 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。args=arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。// 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了// 获得当前事件触发的时间戳letnow= (newDate()).getTime()
// 更新当前最新的定时器计时时间内最后一次事件触发的时间戳timestamp=now;
// 如果定时器不存在且func需要立即执行if (!timeout&&immediate) {
// 通过 apply 修改func的this指向,并让func获取真正的事件函数(即防抖函数return出来的函数)的参数,之后执行funcresult=func.apply(context, args);
context=args=null;
      }
// 如果定时器不存在就创建一个(这里创建的定时器只会在每次wait时间周期内的第一次事件触发时创建,因为这个是setTimeout而不是setInterval,所以当第一次触发事件创建定时器之后,timeout就会一直存在或者被其他定时器覆盖,而不会被清空)if (!timeout) {
timeout=setTimeout(later, wait);
      }
// 如果定时器存在,说明此时处于防抖状态,不需要再次创建新的定时器if (timeout) {
console.log('如果定时器存在,说明此时处于防抖状态,不需要再次创建新的定时器');
      }
// 返回func执行之后的结果returnresult;
    };
  };
// 要执行的事件函数consthandleClick= (e: any) => {
console.log('提交表单', e);
  }
return (
<divonClick={debounce(handleClick, 3000, true)}>提交表单</div>  )
}

节流

什么是节流

防抖动和节流本质是不一样的。防抖动是多次触发但只会执行一次,节流是多次触发但周期内只会执行一次


throttling,节流的策略是,每个时间周期内,不论触发多少次事件,也只执行一次动作。上一个时间周期结束后,又有事件触发,开始新的时间周期,同样新的时间周期也只会执行一次动作。 节流策略也分前缘和延迟两种。与debounce类似,延迟是指周期结束后执行动作,前缘是指执行动作后再开始周期。

延迟throttling示意图:

image.png

前缘throttling 示意图:

image.png

实现节流函数

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:(前缘throttling)

importReactfrom'react'exportdefaultfunctionThrottleTest() {
letthrottle= (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为节流的周期时长letprev: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储一个新周期的起始时间,因此借助闭包缓存新周期的起始时间returnfunction(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错letcontext=this; // 谁调用函数(这里的函数是节流函数return出来的函数),this就指向谁。// 这步不能少,因为原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。letargs=arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是节流函数return出来的函数)。// 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把节流函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了letnow=Date.now(); // 获取当前时间if (!prev||now-prev>=wait) { // 如果当前周期起始时间不存在(那就是第一次触发事件),或者当前周期已经结束console.log('新周期开始');
func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行funcprev=Date.now(); // 更新新周期的起始事件      }
    }
  }
// 要执行的事件函数consthandleClick= (e: any) => {
console.log('提交表单', e);
  }
return (
<divonClick={throttle(handleClick, 3000)}>提交表单</div>  )
}

定时器版:(延迟throttling)

importReactfrom'react'exportdefaultfunctionThrottleTest() {
letthrottle= (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为节流的周期时长lettimeout: any=null;
returnfunction(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错letcontext=this; // 谁调用函数(这里的函数是节流函数return出来的函数),this就指向谁。// 这步不能少,因为原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。letargs=arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是节流函数return出来的函数)。// 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把节流函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了if (!timeout) {
timeout=setTimeout(() => {
timeout=null;
func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行func        }, wait)
      }
    }
  }
// 要执行的事件函数consthandleClick= (e: any) => {
console.log('提交表单', e);
  }
return (
<divonClick={throttle(handleClick, 3000)}>提交表单</div>  )
}

时间戳版本与定时器版本的可配置结合(配置前缘throttling以及延迟throttling):

importReactfrom'react'exportdefaultfunctionThrottleTest() {
constthrottle= (func: any, wait: any, options: any) => { // func为原本事件触发后所需要执行的函数;// wait为节流的周期时长;// options是一个对象,如果想周期内第一次触发事件就执行func(即前缘throttling),传入{leading: false},如果想周期内最后一次触发事件才执行func(即延迟throttling),传入{trailing: false},两者不能共存,否则函数不能执行lettimeout: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例letcontext: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的thisletargs: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。// 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数)letresult: any=null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储func执行之后返回的结果,因此借助闭包缓存func执行之后返回的结果letprevious=0; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储一个新周期的起始时间,因此借助闭包缓存新周期的起始时间// 如果 options 没传则设为空对象if (!options) options= {};
letlater=function() {
previous=options.leading===false?0 : Date.now(); // 如果是设定周期内第一次触发事件就执行func,那么就重置previous为0,否则就将previous设为当前时间timeout=null; // 清空context、args闭包变量,释放内存result=func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行funcif (!timeout) context=args=null; // 清空context、args闭包变量,释放内存    };
returnfunction(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错letnow=Date.now();
if (!previous&&options.leading===false) previous=now; // 如果当前周期起始时间不存在(那就是第一次触发事件),或者当前周期已经结束letremaining=wait- (now-previous);
context=this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。// 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。// 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。args=arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。// 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。// 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了if (remaining<=0||remaining>wait) {
if (timeout) {
clearTimeout(timeout);
timeout=null;
        }
previous=now;
result=func.apply(context, args);
if (!timeout) context=args=null;
      } elseif (!timeout&&options.trailing!==false) {
timeout=setTimeout(later, remaining);
      }
returnresult;
    };
  };
// 要执行的事件函数consthandleClick= (e: any) => {
console.log('提交表单', e);
  }
return (
<divonClick={throttle(handleClick, 3000, {trailing: false})}>提交表单</div>  )
}

防抖与节流注意事项

①使用闭包

为了避免全局的命名污染,因此我们不考虑使用全局变量。同时为了让所需变量能得到缓存,因此我们使用闭包存储部分需要用到的变量


②this指向的问题以及event参数:

因为事件触发调用的,是防抖/节流函数return出来的函数,而不是事件函数func,事件函数func是作为参数传入防抖/节流函数,其this指向是window,而不是触发事件调用函数的dom,同理,自然也拿不到事件自带的event参数,所以我们要通过闭包,存储this上下文以及argument这一传递给函数的参数的类数组对象


实际上,所有的高阶函数,内部都需要注意this的绑定


文章参考

https://blog.csdn.net/hupian1989/article/details/80920324

https://segmentfault.com/a/1190000018428170

https://www.jianshu.com/p/c8b86b09daf0

https://www.cnblogs.com/youma/p/10559331.html

目录
相关文章
|
3月前
|
前端开发 UED 开发者
颠覆你的前端知识:防抖与节流的区别及实战解析!
【8月更文挑战第23天】在Web前端开发中,处理用户界面交互产生的事件可能会影响性能。为此,我们有两种优化方法:防抖(debounce)和节流(throttle)。防抖确保函数仅在事件停止触发一段时间后执行一次,适用于如搜索自动补全场景。而节流则确保函数按固定时间间隔执行,不管用户操作频率如何。本篇技术博客将深入解析两者差异并提供示例代码,帮助开发者更好地理解和应用这些技巧以提升应用性能和用户体验。
78 0
|
1月前
|
前端开发
前端常用方法防抖(debounce)和节流(throttle)的示例演示及应用场景说明
前端常用方法防抖(debounce)和节流(throttle)的示例演示及应用场景说明
21 0
|
2月前
|
缓存 边缘计算 前端开发
关于前端性能优化问题,认识网页加载过程和防抖节流
该文章详细探讨了前端性能优化的方法,包括理解网页加载过程、实施防抖和节流技术来提升用户体验和性能。
|
4月前
|
缓存 开发框架 前端开发
前端性能优化【前端必备】(含懒加载,手写防抖、节流等)
前端性能优化【前端必备】(含懒加载,手写防抖、节流等)
72 4
|
6月前
|
前端开发 JavaScript UED
【专栏】前端开发中,为解决用户操作引发的性能问题,常使用debounce(防抖)和throttle(节流)技术
【4月更文挑战第29天】前端开发中,为解决用户操作引发的性能问题,常使用debounce(防抖)和throttle(节流)技术。Debounce确保在一段时间内只执行最后一次事件触发的操作,减少不必要的执行,但有滞后性,适合搜索框实时搜索。Throttle则保证一定时间间隔内函数执行一次,保持固定频率,适用于滚动事件处理和窗口大小调整。两者可借助JavaScript或第三方库实现,需根据场景和需求选择并调整。正确使用能提升应用性能和用户体验。
48 1
|
6月前
|
前端开发 JavaScript UED
【前端面经】快手二面:节流和防抖知道吗?
【前端面经】快手二面:节流和防抖知道吗?
43 0
|
6月前
|
前端开发 UED
【Web 前端】防抖与节流的区别
【4月更文挑战第22天】【Web 前端】防抖与节流的区别
|
28天前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
119 2
|
28天前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
36 0
|
28天前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。