前言防抖和节流是老生常谈的话题,无论是在面试还是实际开发中,都经常涉及到。本文将介绍防抖与节流的概念、应用场景以及代码实现。代码实现参考了lodash的源码,去掉了对参数类型和运行环境的检测,使代码更加简洁易懂,易学易懂。概念及应用防抖和节流都是为了防止代码被频繁执行。Debounce:指令发出后,开始计时。如果在计时范围内重复发出该指令,则重新开始计时,等待计时完成。执行代码。例如:公交车进站,行人陆续上车。假设等待时间为20秒,只有在20秒内无人上车时,公交车才会开走(执行代码)。Throttle(节流阀):代码执行完毕后,进入冷却时间。在冷却时间内,不会重复执行,等到冷却时间再执行。例如:女神回复每天舔狗一次。就算狗子今天回复了再发消息,女神也只会回复(再次执行代码)到第二天。应用场景在说应用场景之前,先统一一下防抖和节流的概念。throttling就是包括最大时限在内的防抖。说明:假设在100秒内连续频繁的发出命令,防抖的处理结果会在100秒后执行。,但这是非常不友好的。防抖代码中通常会添加最大时间限制。当达到最大时间限制时,即使在等待时间内仍然发出命令,代码也会被执行一次。这变得节流。防抖和节流一般应用于搜索提示、页面滚动等,也用于限制触发频繁且次数不定的事件:mousemove、scroll、resize。当前,浏览器具有过剩的性能。为了良好的用户体验,这些场景基本上都使用了Throttling。代码实现上面也已经提到了。防抖和节流的区别在于是否有最大时间限制。所以在实现上,先实现防抖功能,再加入最大时限,使其成为节流功能。写代码之前,需要明确要实现的函数的参数和返回值。该函数需要传入两个参数,分别是要限制执行频率的函数和延迟时间(以毫秒为单位)。函数的返回值也是一个函数,和传入的函数功能一样,已经去抖的函数在执行期间需要涉及三个函数。为了避免混淆,这里统一命名:我们要实现的函数命名为debounce,下面称之为外部函数debounced。动态函数,即外部函数的返回值,命名为debounced,下面称之为防抖函数。至于内部函数,由于函数不会立即执行,所以应该没有返回值。requestAnimationFrame的感知基于setTimeout,网上有很多防抖/节流功能。lodash源码使用了requestAnimationFrame,其性能和稳定性要优于setTimeout,所以本文也基于requestAnimationFrame实现了requestAnimationFrame()。你需要传入一个函数作为参数,它会在下一次重绘之前执行浏览器。浏览器的重绘频率是每秒60次,大约16ms重绘一次。requestAnimationFrame函数可以简单的看做是一个setTimeout函数,延迟16ms。防抖功能实现防抖功能代码如下。每次调用防抖功能,都会重新开始计时。由于是基于requestAnimationFrame,所以需要递归启动定时器/***@description:*@param{Function}func待去抖函数,内部函数*@param{number}wait等待时间*@return{Function}debouncedfunction*/functiondebounce(func,wait){letlastArgs,//保存参数lastThis,//保存这个timerId,//TimeridlastCallTime//最近调用防抖功能的时间//ResetthetimerfunctionstartTimer(pendingFunc){//取消上次打开的定时器cancelAnimationFrame(timerId)//启动一个新的定时器并返回定时器idreturnrequestAnimationFrame(pendingFunc)}//检查是否到了执行函数的时间shouldInvoke(time){consttimeSinceLastCall=time-lastCallTime//距离上一次防抖功能的调用已经超过等待时间returntimeSinceLastCall>=wait}//调用内部函数functioninvokeFunc(){//获取之前保存的thisand参数constargs=lastArgsconstthisArg=lastThis//this和参数留空,不影响垃圾回收lastArgs=lastThis=undefinedfunc.apply(thisArg,args)}//传入定时器的回调函数//继续获取当前time判断是否应该调用内部函数函数timerExpired(){consttime=Date.now()if(shouldInvoke(time)){//执行内部函数timerId=undefinedinvokeFunc()}else{//递归启动定时器timerId=startTimer(timerExpired)}}//返回的debounce函数,没有返回值functiondebounced(...args){consttime=Date.now()//更新this和参数lastArgs=argslastThis=this//更新debounce函数调用的时间lastCallTime=time//启动定时器timerId=startTimer(timerExpired)}returndebounced}//测试函数letpreTime=Date.now()constfunc=()=>{letnextTime=Date.now()console.log(nextTime-preTime)preTime=nextTime}constdfunc=debounce(func,50)dfunc()dfunc()//50ms后,控制台打印50setTimeout(()=>{dfunc()dfunc()},100)//之后150ms,控制台打印100。节流功能在lodash中实现。节流功能和防抖功能共用一套代码,只是配置参数不同。本文也将复用之前的代码。节流功能是防抖功能的两倍。这个特性节流函数包含最大时间限制(maxWait),这是防抖功能和节流功能的区别。节流函数经常立即执行一次内部函数(leading)。完整代码如下:/***@description:*@param{Function}func函数要防抖,内部函数*@param{number}wait等待时间*@param{number|undefined}max等待最大时限,如果有节流功能,如果没有就是防抖功能*@param{boolean}leading指定延时开始前是否调用内部函数,默认不调用*@return{Function}*/functiondebounce(func,wait,maxWait=undefined,leading=false){letlastArgs,//保存参数lastThis,//保存这个timerId,//TimeridlastCallTime,//防抖功能开启的时间calledlastInvokeTime//内部函数最后被调用初始值为0保证letmaxing=!!maxWait//是否指定最大等待时间//最大时间限制不能小于等待时间if(maxWait){maxWait=Math.max(wait,maxWait)}//重置定时器functionstartTimer(pendingFunc){cancelAnimationFrame(timerId)returnrequestAnimationFrame(pendingFunc)}//检查是否到了执行functionshouldInvoke(time){consttimeSinceLastCall=time-lastCallTimeconsttimeSinceLastInvoke=internaltime-lastInvokeTime//上一次函数时间还没有定义(第一次执行节流函数)//自上次调用防抖函数后已经超过等待时间(防抖的函数shakefunction)//设置了最大时限,自上次调用内部函数后的时间达到最大时限(节流函数的功能)return(lastInvokeTime===undefined||timeSinceLastCall>=等待||(最大化&&timeSinceLastInvoke>=maxWait))}//调用内部函数functioninvokeFunc(time){//获取之前保存的this和参数constargs=lastArgsconstthisArg=lastThis//this和参数为空,不影响垃圾回收lastArgs=lastThis=undefined//更新最新的内部函数的调用时间lastInvokeTime=timefunc.apply(thisArg,args)}//不断获取当前时间判断是否应该调用内部函数functiontimerExpired(){consttime=Date.now()if(shouldInvoke(time)){timerId=undefined//如果内部函数已经立即执行过//并且在等待时间内没有再次调用节流函数//之后不需要再次执行内部函数等待时间if(lastArgs)invokeFunc(time)}else{//重启定时器timerId=startTimer(timerExpired)}}//返回的debounce函数,该函数没有返回值functiondebounced(...args){const时间=Date.now()//这里检查是否应该重启设置定时器constisInvoking=shouldInvoke(time)//更新this和参数lastArgs=argslastThis=this//更新防抖功能调用时间lastCallTime=timeif(isInvoking){if(timerId===undefined){//第一次执行节流函数,更新内部函数调用时间lastInvokeTime=timetimerId=startTimer(timerExpired)//检测leading属性,立即调用内部函数if(leading)invokeFunc(time)}elseif(maxing){//节流函数,执行内部函数并重置定时器timerId=startTimer(timerExpired)invokeFunc(time)}}elseif(timerId===undefined){//内部函数刚刚执行完后调用节流函数//只启动定时器,不需要更新内部函数调用时间timerId=startTimer(timerExpired)}}returndebounced}/***@description:*@param{Function}func函数要防抖,内部函数*@param{number}等待冷却时间*@param{boolean}leading指定是否调用延迟开始前的内部函数,默认调用*@return{Function}*/functionthrottle(func,wait,leading=true){returndebounce(func,wait,wait,leading)}//测试函数letpreTime=Date.now()letarr=[]constfunc=()=>{letnextTime=Date.now()arr.push(nextTime-preTime)preTime=nextTime}consttfunc=throttle(func,100,false)letid=setInterval(tfunc,10)setTimeout(()=>{clearInterval(id)console.log(arr)//[112,101,104,103,100,100,100,101,106,101]},1000)来看一个其他功能实现的业务场景mouseover停在按钮上0.5秒后,会出现该按钮的功能提示。有多个按钮,只显示鼠标悬停按钮的功能提示。很明显是用防抖来实现的。但是,如果用户在0.5秒内移出按钮,则应该不显示提示,但是设置了防抖功能的定时器,0.5秒后仍然显示提示。这是一个明显的错误。所以防抖功能应该有取消定时器的功能。functiondebounce(func,wait,maxWait=undefined,leading=false){...debounced.cancel=function(){//清除定时器if(timerId!==undefined){cancelAnimationFrame(timerId)}//清除variablelastArgs=lastThis=timerId=lastCallTime=lastInvokeTime=undefined}returndebounced}结语相信看完本文,你一定对防抖和节流有了更深的理解。本文代码实现只提取了lodash源码的核心部分,并做了一定的修改。lodash提供的其他配置项都是相关功能,在业务中很少用到,本文不再介绍。有兴趣的可以自己查一下。文中如有不明白或不严谨的地方,欢迎评论提问。如果你喜欢它或者帮助它,我希望你能喜欢它并关注它。鼓励查看作者。
