JavaScript函数节流概念与用法实例详解

本文实例讲述了JavaScript函数节流概念与用法。分享给大家供大家参考,具体如下:

最近在做网页的时候有个需求,就是浏览器窗口改变的时候需要改一些页面元素大小,于是乎很自然的想到了window的resize事件,于是乎我是这么写的

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
   <title>Throttle</title>
</head>
<body>
   <script type= "text/javascript" >
     n=0;
     function resizehandler(){
       console.log( new Date().getTime());
       console.log(++n);
     }
     window.onresize=resizehandler;
   </script>
</body>
</html>

功能倒是实现了,但是我拖拽的方式改变浏览器窗口大小的时候看了下控制台

没错,简单的一个拖拽让我的resizeHandler()方法执行了52次,这完全不是我想要的效果,实际上我的resizeHandler()方法的代码很复杂,甚至会使用ajax向服务器发送请求,要是简单的一次改变窗口大小就要调用52次这还了得

函数节流

其实我的本意只是窗口resize后页面做一些调整就可以,而window的resize事件并不是在resize结束后才触发的,具体则么个频率我也不知道,但却是在不停的调用,直到窗口大小不再变化。其实类似的机制还有鼠标的mousemove,都是在短时间内重复触发。

在《JavaScript高级程序设计》中有专门应对此问题的函数节流

?
1
2
3
4
5
6
function throttle(method,context){
   clearTimeout(method.tId);
   method.tId=setTimeout( function (){
     method.call(context);
   },500);
}

原理很简单,利用定时器,让函数执行延迟500毫秒,在500毫秒内如果有函数又被调用则删除上一次调用,这次调用500毫秒后执行,如此往复。这样我刚才的代码可以改为

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type= "text/javascript" >
n=0;
function resizehandler(){
   console.log( new Date().getTime());
   console.log(++n);
}
function throttle(method,context){
   clearTimeout(method.tId);
   method.tId=setTimeout( function (){
     method.call(context);
   },500);
}
window.onresize= function (){
   throttle(resizehandler,window);
};
</script>

拖拽一下试试,果真只执行了一次

另一种做法

网上还有一种函数节流方案,它是这么做的

?
1
2
3
4
5
6
7
8
9
10
function throttle(method,delay){
   var timer= null ;
   return function (){
     var context= this , args=arguments;
     clearTimeout(timer);
     timer=setTimeout( function (){
       method.apply(context,args);
     },delay);
   }
}

调用一下试试,一样的效果

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type= "text/javascript" >
n=0;
function resizehandler(){
   console.log( new Date().getTime());
   console.log(++n);
}
function throttle(method,delay){
   var timer= null ;
   return function (){
     var context= this , args=arguments;
     clearTimeout(timer);
     timer=setTimeout( function (){
       method.apply(context,args);
     },delay);
   }
}
window.onresize=throttle(resizehandler,500); //因为返回函数句柄,不用包装函数了
</script>

比较

两种方法都是利用了setTimeout,不同的是第二种方法加入的函数延迟执行时间,这个在第一种方案中很容易也具有此功能,加一个参数的事儿。

但第一种方案把tId设为函数的一个变量保存,而第二种创建了一个闭包来存储。个人觉得差距不大,很喜欢第一种,简单,高效。

新需求

有一天做了个类似的东西,就像百度首页输入自动提示一样的东西,我在text上绑定keyup事件,每次键盘弹起的时候自动提示,但是又不想提示那么频繁,于是我用了上面方法,但是悲剧了,只有停止输入等500毫秒才会提示,在输入过程中根本就没有提示。看了一下代码,可不是嘛,只要是用户会盲打,在500毫秒内按一下键盘,提示函数就会不断被延迟,这样只有停下来的时候才会提示,这就没意义了。

能不能在函数节流的基础上间隔固定时间就执行一次?

小改动

在网上搜了一下我们可以根据第二种写法(第一种为函数拓展多个变量感觉有些不好)做些改动,添加一个参数作为到固定间隔必须执行

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function throttle(method,delay,duration){
   var timer= null , begin= new Date();
   return function (){
     var context= this , args=arguments, current= new Date();;
     clearTimeout(timer);
     if (current-begin>=duration){
        method.apply(context,args);
        begin=current;
     } else {
       timer=setTimeout( function (){
         method.apply(context,args);
       },delay);
     }
   }
}

这样每次我们判断间隔了多久,要是超过设置时间则立即执行一次,以刚才例子试一试效果

?
1
window.onresize=throttle(resizehandler,100,200);

果真既没有频繁执行也没有就最后执行


JavaScript 函数节流详解

浏览器一个网页的UI线程只有一个,他同时会处理界面的渲染和页面JavaScript代码的执行(简单扩展一下,浏览器或者JavaScript运行大环境并不是单线程,诸如ajax异步回调、hybrid框架内与native通信、事件队列、CSS运行线程等等都属于多线程环境,不过ES6引入了Promise类来减少了部分异步情况)。因此当JavaScript代码运行计算量很大的方法时,就有可能阻塞UI线程,小则导致用户响应卡顿,严重的情况下浏览器会提示页面无响应是否强制关闭。例如网页的页面滚动事件、移动设备的滑动、缩放事件等。即使没有出现严重的性能问题,我们也应该站在性能优化的角度将短时间内会多次触发的大规模处理时间进行分流计算。

如何有效避免UI线程运行过长的代码,是所有用户交互应用需要考虑的问题,同样的问题在客户端Android可以使用UI主线程开子线程来分散计算。与此对应的,js也可以通过引入webWorker来分散计算,但是在js中有一个更简单并且效果不错的方法:函数节流。使用函数节流的核心技巧就是使用定时器分段计算。具体的实现方式大致有两种思路。

·方法一

1.这种实现方式的思路很好理解:设置一个一间隔时间,比如50毫秒,以此时间为基准设置定时器,当第一次触发事件到第二次触发事件间隔小于50毫秒时,清除这个定时器,并设置一个新的定时器,以此类推,直到有一次事件触发后50毫秒内没有重复触发。代码如下:

?
1
2
3
4
5
6
function debounce(method){
   clearTimeout(method.timer);
   method.timer=setTimeout(function(){
    method();
   }, 50 );
}

这种设计方式有一个问题:本来应该多次触发的事件,可能最终只会发生一次。具体来说,一个循序渐进的滚动事件,如果用户滚动太快速,或者程序设置的函数节流间隔时间太长,那么最终滚动事件会呈现为一个很突然的跳跃事件,中间过程都被节流截掉了。这个例子举的有点夸张了,不过使用这种方式进行节流最终是会明显感受到程序比不节流的时候“更突兀”,这对于用户体验是很差的。有一种弥补这种缺陷的设计思路。

·方法二

2.第二种实现方式的思路与第一种稍有差别:设置一个间隔时间,比如50毫秒,以此时间为基准稳定分隔事件触发情况,也就是说100毫秒内连续触发多次事件,也只会按照50毫秒一次稳定分隔执行。代码如下:

?
1
2
3
4
5
6
7
8
9
var oldTime= new Date().getTime();
var delay= 50 ;
function throttle1(method){
   var curTime= new Date().getTime();
   if (curTime-oldTime>=delay){
    oldTime=curTime;
    method();
   }
}

相比于第一种方法,第二种方法也许会比第一种方法执行更多次(有时候意味着更多次请求后台,即更多的流量),但是却很好的解决了第一种方法清除中间过程的缺陷。因此在具体场景应根据情况择优决定使用哪种方法。

对于方法二,我们再提供另一种同样功能的写法:

?
1
2
3
4
5
6
7
8
9
10
var timer=undefined,delay= 50 ;
function throttle2(method){
   if (timer){
     return ;
   }
   method();
   timer=setTimeout(function(){
     timer=undefined;
   },delay);
}

最后说点个外话,说明一下函数节流的名称问题,大家往往会看到throttle和debounce两个方法名,throttle可以译为“节制,卡住”,debounce可以译为“防反跳”。在《JavaScript高级程序设计》中作者介绍了方法一,并且作者使用了“throttle”这个函数名。而在《第三方JavaScript编程》书中同时出现了方法一和方法二,作者将方法一命名为“debounce”,将方法二命名为“throttle”。国内在同时介绍两个方法的时候有些文章错误的将方法一命名为“throttle”,而将方法二命名为“debounce”,从英语的角度来说是很不负责任的。因此在这里拨乱反正:方法一适合理解为“防反跳”,应命名为“debounce”;方法二适合理解为“函数节制”,应命名为“throttle”。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值