JS防抖和节流

对防抖和节流的一些理解,做一次记录。(之前项目中的需求是在输入框中输入内容之后,调接口返回值,然后不知道还有节流这波操作,然后就写了判断当鼠标失去焦点的时候调接口,后来大佬说可以使用节流来实现)

防抖和节流算起来应该属于性能优化的知识,但是处理不当或者是放任不管就容易引起浏览器卡死。就是在绑定scroll、resize这类事件时,当他发生时,被触发的频率非常高,间隔很近。如果事件中涉及到大量的位置计算、DOM操作、元素重绘等工作且这些工作无法在下一个scroll事件触发前完成,就会造成浏览器调帧。加之用户鼠标滚动往往时连续的,就会持续触发scroll事件导致调帧扩大、浏览器CPU使用率增加、用户体验受到影响。尤其时在涉及与后端的交互中,前端依赖于某中事件如resize、scroll,发送http请求,在这个过程中,如果不做防抖处理,那么在事件触发的一瞬间,就会有很多个请求发过去,增加了服务端的压力。

1.从滚动条监听的例子说起

先说一个常见的功能,很多网站会提供一个按钮:用于返回顶部。

这个按钮只会在滚动到距离顶部一定位置的时候才会出现,那么现在抽象出这个功能需求 --- 监听滚动条事件,返回当前滚条和顶部的距离。

这个需求很简单,直接写:

1 function showTop  () {
2     var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
3   console.log('滚动条位置:' + scrollTop);
4 }
5 window.onscroll  = showTop
View Code

但是:在运行的时候会发现:这个函数的默认执行频率太高了!以chrome为例,我们可以点击选中一个页面的滚动条,然后点击一次键盘的【向下方向键】,会发现函数执行了8-9次

然而实际上并不需要如此高频的反馈,毕竟浏览器的性能是有限的,不应该浪费在这里,所以需要优化这种场景。

2.防抖

基于上述的场景,首先提出第一种思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值:300ms

  • 如果在300ms内没有再次触发滚动事件,那么就执行函数。

  • 如果在300ms内再次触发滚动事件,那么当前的即使取消,重新开始计时。

效果就是:如果在短时间内大量触发同意事件,只会执行一次函数。

实现:既然前面都提到了计时,那实现的关键就在于setTimeOut这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现:

 1 /**
 2 * fn[function] 需要防抖的函数
 3 * delay[number] 毫秒,防抖期限值
 4 */
 5 function debounce(fn,delay){
 6   let timer = null;
 7   return function(){
 8     if(timer){
 9       //进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
10       clearTimeout(timer)
11       timer = setTimeOut(fn,delay)
12     }else{
13       // 进入该分支说明当前并没有在计时,那么就开始一个计时
14       timer = setTimeOut(fn,delay)
15     }
16   }
17 }
View Code

当然 上述代码是为了贴合思路,方便理解。写完会发现其实timer = setTimeOut(fn,delay)是一定会执行的,所以可以稍微简化下:

 1 function debounce(fn,delay){
 2     let timer = null //借助闭包
 3     return function() {
 4         if(timer){
 5             clearTimeout(timer) 
 6         }
 7         timer = setTimeout(fn,delay) // 简化写法
 8     }
 9 }
10 // 然后是旧代码
11 function showTop  () {
12     var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
13   console.log('滚动条位置:' + scrollTop);
14 }
15 window.onscroll = debounce(showTop,1000) // 为了方便观察效果我们取个大点的间断值,实际使用根据需要来配置
View Code

此时会发现,必须在停止滚动1s以后,才会打印出滚动条位置。

防抖也就实现了:定义即:

  • 对于短时间内连续触发的事件(上面的滚动事件),防抖的含义就是让某个时间期限(如上面的1000毫秒)内,事件处理函数只执行一次。

3.节流

继续思考,使用上面的防抖方案来处理问题的结果是:

  • 如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离。

但是如果产品同学的期望处理方案是:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈呢?

其实很简单:我们可以设计一种类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活(类似于技能冷却时间)。

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

实现 这里借助setTimeout来做一个简单的实现,加上一个状态位valid来表示当前函数是否处于工作状态:

 

定时器方案

 1 function throttle(fn,delay){
 2   let valid = true;
 3   return function(){
 4        if(!valid){
 5          return false;
 6         }
 7        //执行函数并且在间隔期间内把状态位设为无效
 8            valid = false;
 9         setTimeout(()=>{
10           fn()
11           valid = true;
12         },delay)
13     }
14 }
15 /* 请注意,节流函数并不止上面这种实现方案,
16    例如可以完全不借助setTimeout,可以把状态位换成时间戳,然后利用时间戳差值是否大于指定间隔时间来做判定。
17    也可以直接将setTimeout的返回的标记当做判断条件-判断当前定时器是否存在,如果存在表示还在冷却,并且在执行fn之后消除定时器表示激活,原理都一样
18     */
19 // 以下照旧
20 function showTop  () {
21     var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
22   console.log('滚动条位置:' + scrollTop);
23 }
24 window.onscroll = throttle(showTop,1000) 
View Code

运行以上代码的结果:

如果一直拖着滚动条进行滚动,那么会以1s的时间间隔,持续输出当前位置和顶部的距离。

时间戳方案

 1 var throttle = function(fn,delay){
 2   var prev = Date.now();
 3   return function(){
 4     var context = this;
 5     var args = arguments;
 6     var now = Date.now();
 7     if(now -prev >=delay){
 8        fn.apply(context,args)
 9        prev = Date.now();
10        }
11   }
12 }
13 function handle(){
14   console.log(Math.random());
15 }
16 window.addEventListener('scroll',throttle(handle,1000));
View Code

时间戳+定时器

 1 var throttle = function(func, delay) {
 2      var timer = null;
 3      var startTime = Date.now();
 4      return function() {
 5              var curTime = Date.now();
 6              var remaining = delay - (curTime - startTime);
 7              var context = this;
 8              var args = arguments;
 9              clearTimeout(timer);
10               if (remaining <= 0) {
11                     func.apply(context, args);
12                     startTime = Date.now();
13               } else {
14                     timer = setTimeout(func, remaining);
15               }
16       }
17 }
18 function handle() {
19       console.log(Math.random());
20 }
21  window.addEventListener('scroll', throttle(handle, 1000));
View Code

4.其他应用场景举例

讲完了这两个技巧,下面介绍一下平时开发中常遇到的场景:

  1. 搜索框input事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求。

  2. 页面resize事件,常见于需要做页面适配的时候。需要根据最终呈现的页面情况进行dom渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)

5.总结

函数防抖:将几次操作合并为一个操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay内再次触发的话,就会取消之前的计时器而重新设置。这样一来。只有最后一次操作能被触发。

函数节流:使得一定时间内只触发一次函数,原理是通过判断是否到达一定时间来触发函数。

区别:函数节流不管事件触发多频繁,都会保证在规定的时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。比如在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次ajax请求,而不是在啊用户停下滚动页面操作时才去请求数据。这种场景就适合用节流技术来实现。

posted @ 2019-07-31 14:15  ichthyo-plu  阅读(201)  评论(0编辑  收藏  举报