节流与防抖

  • 节流与防抖已关闭评论
  • 87 次浏览
  • A+
所属分类:Web前端
摘要

本文可以配合本人录制的视频一起食用节流和防抖是前端开发中常用的优化技术,主要用于优化一些高频触发的事件。

本文可以配合本人录制的视频一起食用

作用

节流和防抖是前端开发中常用的优化技术,主要用于优化一些高频触发的事件。

字面理解

节流与防抖,先从字面上理解一下,节流就是节制流入或流出,在前端方面我个人理解一下,指的是节制功能或请求的触发次数,所以节流函数字面上的意思就是防止功能或请求被频繁触发的函数;防抖呢,更好理解,防止抖动,它的字面意思更贴近前端的需求,就是防止页面抖动,以达到更好的用户体验。

适用场景

从字面上的理解可以联想到分别适合这两个功能的场景

先看节流,比如我们打开搜索引擎页面,百度或者Google,当我们在搜索框输入内容,会出现自动补全的下拉框,下拉框里的数据是请求接口获取的,如果不加以限制,就会在频繁输入的时候发送出大量请求,所以节流就可以应用在这类场景中。

再看防抖,当我们在快速上下滚动页面的过程中,如果页面滚动行为绑定了事件监听器,就可能频繁触发回调导致大量的计算从而引发页面的抖动甚至卡顿,防抖函数就可以应用在这类场景中。

所以总体来说,节流和防抖都是用于控制事件触发的频率,只是控制的点不同

防抖更适合于反馈较快的场景,就是说用户操作之后很快就会有反馈,我们不希望反馈太快,并且不希望频繁操作导致要去处理太多的反馈(合并处理);而节流更适合耗时较久的场景,就仿佛某个人在说省点流量吧,我不是没反馈,只是需要多点时间来处理,不要频繁给我发送相同的操作指令。

实现

根据以上理解,我们可以分别来实现这两个函数。

节流

首先是节流。

节流是在某次事件触发时执行指定操作后,再次触发事件时,若两次事件的触发时间点的间隔不小于给定的时间间隔,就再次执行指定操作,否则就不执行。

function throttle(fn, interval) {   // fn是待执行的操作,interval是给定的时长,在给定的时长内只发送一次操作指令,也就是说只执行一次fn   // 设置一个变量用于记录   let last = 0; // 记录上次动作的执行时间   return function() {     // 首先保留调用时的this上下文和传入的参数     let context = this;     let args = arguments;     // 记录当前事件触发的时间点     let now = Date.now();     // 检查当前时间点与上次执行操作的时间点之间的间隔     if (now - last >= interval) {       // 如果当前时间与上次触发动作的时间间隔大于或等于interval       // 就触发操作       fn.apply(context, args);       // 并且更新last为当前时间       last = now;     }     // 否则就不做任何操作,即两次事件触发的时间间隔小于interval时,就不触发fn执行,保证在interval设置的时长内只执行一次fn   } } 

我们可以在页面上测试一下

<button id="requestButton">   点我请求 </button> <script> // 用throttle包装click的回调,防止频繁请求 const better_request = throttle(() => {   console.log(Date.now()); }, 3000); document.querySelector('#requestButton').addEventListener('click', better_request); </script> 

防抖

然后是防抖。

防抖就是在频繁触发事件后,等不再触发事件时合并执行动作。

function debounce(fn, delay) {   // fn是待执行的操作,delay是指延迟的时长,我们希望在给定的延时之后再执行fn   // 在防抖函数中需要设置一个定时器,用于延迟执行fn   let timer = null;      return function() {     // 保留调用时的this上下文和传入的参数     let context = this;     let args = arguments;          // 每次事件被触发时,都去清除之前的旧定时器     if (timer) clearTimeout(timer);     // 设定新定时器     // 在给定的delay延时之后,fn才会被执行     // 当事件首次被触发,fn会在delay毫秒后执行     timer = setTimeout(() => {       fn.apply(context, args);     }, delay);   } } 
  1. 使用防抖后,在事件触发时,fn在delay毫秒的延迟后才会执行,可以保证回调反馈不会太快
  2. 如果在delay毫秒内,比如第x毫秒时第二次事件回调被触发,此时前一个fn还未被执行,若不清理计时,第二个fn操作会在delay毫秒后被执行,这样就会导致delay毫秒内有两个fn会被触发;
    1. 第一个fn在delay-x毫秒后执行
    2. 第二个fn在delay毫秒后执行
    3. 两个fn的执行间隔理论上为x毫秒,x小于delay
  3. 所以为了保证fn不被频繁执行,我们要将前一个计时清理掉,使得delay延时内只有一个fn将被执行,相当于将多个反馈合并处理
  4. 如果delay延时内再无事件触发,则延时结束后fn就被执行

这样做看上去似乎没有问题,但实际上是存在问题的,问题就在于如果用户操作过于频繁,就会导致fn的执行被无限推迟,因为新的事件触发总会清除掉上一次的计时器,这样用户的操作需要很久才得到反馈,或者根本得不到反馈,比如用户在频繁滚动页面后,没等到fn执行就跳转其他页面了。

合并版

为了保证在给定的时间内必须执行一次fn,我们可以使用throttle来优化防抖,也可以说是两者的合并。

最终要达到的目标:

  1. 将多次事件触发的fn操作合并执行
  2. 在给定的时间间隔内一定会执行一次fn
function enhanceThrottle(fn, delay) {   // 设置两个变量   // last用于记录上一次fn执行的时间   // timer用于延迟执行fn   let last = 0, timer = null;      return function() {     // 保留回调时的this上下文和传入的参数     let context = this;     let args = arguments;     // 记录当前事件触发的时间点     let now = Date.now();     if (now - last < delay) {       // 如果当前时间点与上一次fn执行时间的间隔小于给定的时间间隔       // 不执行fn操作       // 重置定时器,在delay延时后执行fn       // 这样执行两次fn预计的时间差就是now - last + delay,也就是说时间差会大于delay       clearTimeout(timer);       timer = setTimeout(() => {         last = Date.now();         fn.apply(context, args);       }, delay);     } else {       // 当前时间点与上一次fn执行时间的间隔超出给定的时间间隔       // 就立即执行一次fn       fn.apply(context, args);       // 并更新last的值       last = now;     }   } } 

优化之后,在第一次触发事件时,就会立即执行一次fn。

但是这样优化之后依旧存在问题:

就是在else语句这个分支,当前时间点与上一次fn执行时间的间隔超出给定的时间间隔,就立即执行一次fn,假设此次事件的触发时间点是now2,上一次事件的触发时间点是now1,如果经过now1-last+delay这个延迟之后刚好是now2,就会在立即执行fn的同时,有个延迟的fn也要执行。

可以继续优化,在立即执行fn这个分支里,也去重置计时,clearTimeout(timer),当然实践中可能还是会有问题,比如在清理计时器之前这个延迟的fn操作已经进入任务队列了。

对比

两个初始版的节流和防抖。

看上去,节流函数就像在一段时间间隔的开始时间点执行操作,防抖函数像是在一段时间间隔内最后一次事件触发后执行操作。两者似乎是一个头一个尾,但其实上并没有很相似,节流的时间间隔是给定的,而防抖的时间间隔是不确定的,而是视用户的操作而定。

也就是说节流直接丢掉后面的操作,防抖更类似于合并了前面的操作