国际太阳娱乐网站2138函数节流

国际太阳娱乐网站2138 9

浅谈javascript函数节流

2016/03/14 · JavaScript
· 函数

原文出处:
涂根华   

什么是函数节流?

    
函数节流简单的来说就是不想让该函数在很短的时间内连续被调用,比如我们最常见的是窗口缩放的时候,经常会执行一些其他的操作函数,比如发一个ajax请求等等事情,那么这时候窗口缩放的时候,有可能连续发多个请求,这并不是我们想要的,或者是说我们常见的鼠标移入移出tab切换效果,有时候连续且移动的很快的时候,会有闪烁的效果,这时候我们就可以使用函数节流来操作。大家都知道,DOM的操作会很消耗或影响性能的,如果是说在窗口缩放的时候,为元素绑定大量的dom操作的话,会引发大量的连续计算,比如在IE下,过多的DOM操作会影响浏览器性能,甚至严重的情况下,会引起浏览器崩溃的发生。这个时候我们就可以使用函数节流来优化代码了~

函数节流的基本原理:

    
使用一个定时器,先延时该函数的执行,比如使用setTomeout()这个函数延迟一段时间后执行函数,如果在该时间段内还触发了其他事件,我们可以使用清除方法
clearTimeout()来清除该定时器,再setTimeout()一个新的定时器延迟一会儿执行。

我们先来看一个简单的window.resize的demo例子,比如我先定义一个全局变量count=0;当我触发一次window.resize的时候,该全局变量count++;
我们来看看在控制台中打印出count的效果;JS代码如下:

var count = 0; window.onresize = function(){ count++;
console.log(count); }

1
2
3
4
5
var count = 0;
window.onresize = function(){
    count++;
    console.log(count);
}

执行截图效果如下:

国际太阳娱乐网站2138 1

如上resize的代码,简单的缩放一次就打印出多次,这并不是我们想要的效果,这是简单的测试,那如果我们换成ajax请求的话,那么就会缩放一次窗口会连续触发多次ajax请求,下面我们试着使用函数节流的操作试试一下;

函数节流的第一种方案封装如下:

function throttleFunc(method,context){ clearTimeout(method.tId);
method.tId = setTimeout(function(){ method.call(context); },100); }

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

我们再来封装一下窗口缩放的demo

var count = 0; function myFunc() { count++; console.log(count); }
window.onresize = function(){ throttleFunc(myFunc); } function
throttleFunc(method,context){ clearTimeout(method.tId); method.tId =
setTimeout(function(){ method.call(context); },100); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var count = 0;
function myFunc() {
   count++;
   console.log(count);
}
window.onresize = function(){
    throttleFunc(myFunc);
}
function throttleFunc(method,context){
     clearTimeout(method.tId);
     method.tId = setTimeout(function(){
         method.call(context);
     },100);
}

如上代码,我们再来看看效果,窗口缩放和放大效果会看到,只执行了一次;打印了一次。

上面的代码使用一个定时器每隔100毫秒执行一次;

我们也可以使用闭包的方法对上面的函数进行再封装一下;

函数节流的第二种封装方法如下:

function throttle(fn, delay){ var timer = null; return function(){ var
context = this, args = arguments; clearTimeout(timer); timer =
setTimeout(function(){ fn.apply(context, args); }, delay); }; };

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

上面第二种方案是使用闭包的方式形成一个私有的作用域来存放定时器timer,第二种方案的timer是通过传参数的形式引入的。

调用demo代码如下:

var count = 0; function myFunc() { count++; console.log(count); } var
func = throttle(myFunc,100); window.onresize = function(){ func(); }
function throttle(fn, delay){ var timer = null; return function(){ var
context = this, args = arguments; clearTimeout(timer); timer =
setTimeout(function(){ fn.apply(context, args); }, delay); }; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var count = 0;
function myFunc() {
    count++;
    console.log(count);
}
var func = throttle(myFunc,100);
window.onresize = function(){
   func();
}        
function throttle(fn, delay){
     var timer = null;
     return function(){
         var context = this,
             args = arguments;
         clearTimeout(timer);
         timer = setTimeout(function(){
             fn.apply(context, args);
         }, delay);
     };
};

函数节流的基本思想是:就是想让一个函数不要执行的太频繁,减少一些过快的来节流函数,比如当我们改变窗口缩放的时候,浏览器的间隔有可能是16ms,这是浏览器自带的时间间隔,我们无法改变,而我们通过节流的方式可以试着改变一下这个间隔,尽量稍微延长下这个调用时间,因此我们可以封装如下函数:

函数节流的第三种封装方法

function throttle3(fn,delay,runDelay){ var timer = null; var t_start;
return function(){ var context = this, args = arguments, t_cur = new
Date(); timer & clearTimeout(timer); if(!t_start) { t_start = t_cur;
} if(t_cur – t_start >= runDelay) { fn.apply(context,args);
t_start = t_cur; }else { timer = setTimeout(function(){
fn.apply(context,args); },delay); } } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function throttle3(fn,delay,runDelay){
      var timer = null;
      var t_start;
      return function(){
         var context = this,
             args = arguments,
             t_cur = new Date();
         timer & clearTimeout(timer);
         if(!t_start) {
             t_start = t_cur;
         }
         if(t_cur – t_start >= runDelay) {
              fn.apply(context,args);
              t_start = t_cur;
         }else {
              timer = setTimeout(function(){
                  fn.apply(context,args);
               },delay);
         }
    }
}

调用demo如下:

var count = 0; function myFunc() { count++; console.log(count); } var
func = throttle3(myFunc,50,100); window.onresize = function(){ func();}
function throttle3(fn,delay,runDelay){ var timer = null; var t_start;
return function(){ var context = this, args = arguments, t_cur = new
Date(); timer & clearTimeout(timer); if(!t_start) { t_start = t_cur;
} if(t_cur – t_start >= runDelay) { fn.apply(context,args);
t_start = t_cur; }else { timer = setTimeout(function(){
fn.apply(context,args); },delay); } } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var count = 0;
function myFunc() {
   count++;
   console.log(count);
}
var func = throttle3(myFunc,50,100);
window.onresize = function(){
   func();}
function throttle3(fn,delay,runDelay){
      var timer = null;
      var t_start;
      return function(){
          var context = this,
              args = arguments,
              t_cur = new Date();
          timer & clearTimeout(timer);
          if(!t_start) {
              t_start = t_cur;
          }
          if(t_cur – t_start >= runDelay) {
                fn.apply(context,args);
                t_start = t_cur;
          }else {
                timer = setTimeout(function(){
                     fn.apply(context,args);
                },delay);
          }
      }
}

上面的第三个函数是封装后的函数,有三个参数,我们可以自己设置触发事件的时间间隔,则意味着,如上代码50ms连续调用函数,后一个调用会把前一个调用的等待处理掉,但每隔100ms会至少执行一次,具体使用哪一种方式只要看自己的权衡,但是我个人觉得第二种封装函数的方式够我们使用的,当然据说第三种方式性能更好~

1 赞 3 收藏
评论

国际太阳娱乐网站2138 2

函数节流场景

什么是函数节流?

介绍前,先说下背景。在前端开发中,有时会为页面绑定resize事件,或者为一个页面元素绑定拖拽事件(其核心就是绑定mousemove),这种事件有一个特点,就是用户不必特地捣乱,他在一个正常的操作中,都有可能在一个短的时间内触发非常多次事件绑定程序。而大家知道,DOM操作时很消耗性能的,这个时候,如果你为这些事件绑定一些操作DOM节点的操作的话,那就会引发大量的计算,在用户看来,页面可能就一时间没有响应,这个页面一下子变卡了变慢了。甚至在IE下,如果你绑定的resize事件进行较多DOM操作,其高频率可能直接就使得浏览器崩溃。

怎么解决?函数节流就是一种办法。话说第一次接触函数节流(throttle),还是在看impress源代码的时候,impress在播放的时候,如果窗口大小发生改变(resize),它会对整体进行缩放(scale),使得每一帧都完整显示在屏幕上:

国际太阳娱乐网站2138 3

稍微留心,你会发现,当你改变窗体大小的时候,不管你怎么拉,怎么拽,都没有立刻生效,而是在你改变完大小后的一会儿,它的内容才进行缩放适应。看了源代码,它用的就是函数节流的方法。

函数节流,简单地讲,就是让一个函数无法在很短的时间间隔内连续调用,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。以impress上面的例子讲,就是让缩放内容的操作在你不断改变窗口大小的时候不会执行,只有你停下来一会儿,才会开始执行。

 

例如:实现一个原生的拖拽功能(如果不用H5 Drag和Drop
API),我们就需要一路监听mousemove事件,在回调中获取元素当前位置,然后重置dom的位置。如果我们不加以控制,每移动一定像素而出发的回调数量是会非常惊人的,回调中又伴随着DOM操作,继而引发浏览器的重排和重绘,性能差的浏览器可能会直接假死。这时,我们就需要降低触发回调的频率,比如让它500ms触发一次或者200ms,甚至100ms,这个阀值不能太大,太大了拖拽就会失真,也不能太小,太小了低版本浏览器可能会假死,这时的解决方案就是函数节流【throttle】。函数节流的核心就是:让一个函数不要执行得太频繁,减少一些过快的调用来节流。

函数节流的原理

函数节流的原理挺简单的,估计大家都想到了,那就是定时器。当我触发一个时间时,先setTimout让这个事件延迟一会再执行,如果在这个时间间隔内又触发了事件,那我们就clear掉原来的定时器,再setTimeout一个新的定时器延迟一会执行,就这样。

 

函数去抖场景

代码实现

明白了原理,那就可以在代码里用上了,但每次都要手动去新建清除定时器毕竟麻烦,于是需要封装。在《JavaScript高级程序设计》一书有介绍函数节流,里面封装了这样一个函数节流函数:

 

function throttle(method, context) {

     clearTimeout(methor.tId);

     method.tId = setTimeout(function(){

         method.call(context);

     }, 100);

}

它把定时器ID存为函数的一个属性(=
=个人的世界观不喜欢这种写法)。而调用的时候就直接写

 

window.onresize = function(){

    throttle(myFunc);

}

这样两次函数调用之间至少间隔100ms。

而impress用的是另一个封装函数:

 

var throttle = function(fn, delay){

var timer = null;

return function(){

var context = this, args = arguments;

clearTimeout(timer);

timer = setTimeout(function(){

fn.apply(context, args);

}, delay);

};

};

它使用闭包的方法形成一个私有的作用域来存放定时器变量timer。而调用方法为

 

1
window.onresize = throttle(myFunc, 100);

两种方法各有优劣,前一个封装函数的优势在把上下文变量当做函数参数,直接可以定制执行函数的this变量;后一个函数优势在于把延迟时间当做变量(当然,前一个函数很容易做这个拓展),而且个人觉得使用闭包代码结构会更优,且易于拓展定制其他私有变量,缺点就是虽然使用apply把调用throttle时的this上下文传给执行函数,但毕竟不够灵活。

 

例如:对于浏览器窗口,每做一次resize操作,发送一个请求,很显然,我们需要监听resize事件,但是和mousemove一样,每缩小(或者放大)一次浏览器,实际上会触发N多次的resize事件,这时的解决方案就是节流【debounce】。函数去抖的核心就是:在一定时间段的连续函数调用,只让其执行一次

接下来是?

接下来就讨论怎么更好地封装?这多没意思啊,接下来讨论下怎样拓展深化函数节流。

函数节流让一个函数只有在你不断触发后停下来歇会才开始执行,中间你操作得太快它直接无视你。这样做就有点太绝了。resize一般还好,但假如你写一个拖拽元素位置的程序,然后直接使用函数节流,那恭喜你,你会发现你拖动时元素是不动的,你拖完了,它直接闪到终点去。

其实函数节流的出发点,就是让一个函数不要执行得太频繁,减少一些过快的调用来节流。当你改变浏览器大小,浏览器触发resize事件的时间间隔是多少?我不清楚,个人猜测是16ms(每秒64次),反正跟mousemove一样非常太频繁,一个很小的时间段内必定执行,这是浏览器设好的,你无法直接改。而真正的节流应该是在可接受的范围内尽量延长这个调用时间,也就是我们自己控制这个执行频率,让函数减少调用以达到减少计算、提升性能的目的。假如原来是16ms执行一次,我们如果发现resize时每50ms一次也可以接受,那肯定用50ms做时间间隔好一点。

而上面介绍的函数节流,它这个频率就不是50ms之类的,它就是无穷大,只要你能不间断resize,刷个几年它也一次都不执行处理函数。我们可以对上面的节流函数做拓展:

 

var throttleV2 = function(fn, delay, mustRunDelay) {
var timer = null;
var t_start;
return function() {
var context = this, args = arguments, t_curr = +new Date();
clearTimeout(timer);
if (!t_start) {
t_start = t_curr;
}
if (t_curr – t_start >= mustRunDelay) {
fn.apply(context, args);
t_start = t_curr;
} else {
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
};
};

在这个拓展后的节流函数升级版,我们可以设置第三个参数,即必然触发执行的时间间隔。如果用下面的方法调用

 

1
window.onresize = throttleV2(myFunc, 50, 100);

则意味着,50ms的间隔内连续触发的调用,后一个调用会把前一个调用的等待处理掉,但每隔100ms至少执行一次。原理也很简单,打时间tag,一开始记录第一次调用的时间戳,然后每次调用函数都去拿最新的时间跟记录时间比,超出给定的时间就执行一次,更新记录时间。

狠击这里查看测试页面

到现在为止呢,当我们在开发中遇到类似的问题,一个函数可能非常频繁地调用,我们有了几个选择:一呢,还是用原来的写法,频繁执行就频繁执行吧,哥的电脑好;二是用原始的函数节流;三则是用函数节流升级版。不是说第一种就不好,这要看实际项目的要求,有些就是对实时性要求高。而如果要求没那么苛刻,我们可以视具体情况使用第二种或第三种方法,理论上第二种方法执行的函数调用最少,性能应该节省最多,而第三种方法则更加地灵活,你可以在性能与体验上探索一个平衡点。

 

函数节流的实现

你怎么了,性能

(原谅我,写得有点长 = = ,文章主体还剩最后这一节。)

我们经常说我优化了代码了,现在的代码更高效了,但貌似很少有人去测试,性能是否真的提升了,提升了多少。当然,前端性能测试的不完善、不够体系化也是原因之一,但我们也要有一种严谨的态度。上面介绍了三种方法,理论上来说呢,第一种方法执行的运算最多,性能理应最差(运算过多过频,内存、cpu占用高,页面变卡),而第二种应该是性能最好,第三种就是一种居中的方案。

为了给读者一个更确切的分析,于是我对三种方法做了一次蛋疼的性能测试。。。我选择的是拖拽一个页面元素位置的应用场景,为了让性能优化更明显一点,拖拽的是一个iframe,iframe里面加载的是腾讯首页(一般门户网站的首页都够重量级的),这样在拖拽的过程中会不断触发浏览器的重绘。至于怎么看性能,我打开的是chrome的调试面板的时间线标签,里面有memory监视。对于性能的评价标准,我选的是内存占用。

于是长达两三个小时的性能测试开始了。。。

 

很快我就发现,chrome的性能优化得太好了,我的第一种测试方案三种方法之间有性能差异,但这个差异实在不明显,而且每一轮的测试都有波动,而且每次测试还很难保证测试的背景条件(如开始时的内存占用情况),第一组测试结果如下:

第一种方法:国际太阳娱乐网站2138 4

第二种方法:国际太阳娱乐网站2138 5

第三种方法:国际太阳娱乐网站2138 6

可以发现,这些小差异很难判定哪种方法更好。

 

于是有了新一轮测试。不够重量化?好吧,我每次mousemove的处理函数中,都触发iframe的重新加载;测试数据有瞬时波动?这次我一个测试测60秒,看一分钟的总体情况;测试条件不够统一?我规定在60秒里面mouse
up 6次,其他时间各种move。

于是有了第二组图片(其实做了很多组图片,这里只选出比较有代表性的一组,其他几组类似)

第一种方法:国际太阳娱乐网站2138 7

第二种方法:国际太阳娱乐网站2138 8

第三种方法:国际太阳娱乐网站2138 9

看错了?我一开始也这么认为,但测试了几次都发现,第一种方法正如预料中的占资源,第二种方法竟然不是理论上的性能最优,最优的是第三种方法!

仔细分析。第一种方法由于不断地mousemove,不断更新位置的同时重新加载iframe的内容,所以内存占用不断增加。第二种方法,即原始的函数节流,可以从截图看出内存占用有多处平坦区域,这是因为在mousemove的过程中,由于时间间隔短,不触发处理函数,所以内存也就有一段平滑期,几乎没有增长,但在mouseup的时候就出现小高峰。第三种方法呢,由于代码写了每200ms必须执行一次,于是就有很明显的高峰周期。

为什么第三种方法会比第二种方法占用内存更小呢?个人认为,这跟内存回收有关,有可能chrmoe在这方面真的优化得太多(。。。)。不断地每隔一个小时间段地新建定时器,使得内存一直得不到释放。而使用第三种方法,从代码结构可以看出,当到了指定的mustRunDelay必须执行处理函数的时候,是不执行新建定时器的,即是说在立即执行之后,有那么一小段时间空隙,定时器是被clear的,只有在下一次进入函数的时候才会重新设置。而chrome呢,就趁这段时间间隙回收垃圾,于是每一个小高峰后面都有一段瞬时的“下坡”。

当然,这只是我的推测,期待读者有更独到的看法。

重度测试页面(个人测试的时候是没有切换器的,每次代码选了一种模式,然后就关闭浏览器,重新打开页面来测试,以保证运行时不受到别的模式的影响。这里提供的测试页面仅供参考)

 

函数节流的第一种方案封装如下

后语

(这是后语,不算正文的小节)

上面就是我对函数节流的认识和探索了,时间有限,探索得不够深也写得不够好。个人建议,在实际项目开发中,如果要用到函数节流来优化代码的话,函数节流升级版更加地灵活,且在一些情况下内存占用具有明显的优势(我只试了chrome,只试了两三个钟,不敢妄言)。

最后我们可以整合了第二、三种方法,封装成一个函数,其实第二种方法也就是第三种方法的特例而已。还可以以hash对象封装参数:执行函数、上下文、延迟、必须执行的时间间隔。这比较简单就不在这里贴出来了。

 

原创文章转载请注明:

转载自AlloyTeam:

functionthrottleFunc(method,context){ 
clearTimeout(method.timer);//为什么选择setTimeout
而不是setIntervalmethod.timer = setTimeout(function(){   
method.call(context);  },100);}

看一个封装的demo

window.onscroll =function(){ 
throttleFunc(show);}functionshow(){console.log(1);}functionthrottleFunc(method){ 
clearTimeout(method.timer);  method.timer = setTimeout(function(){   
method();  },100);}

也可以使用闭包的方法对上面的函数进行再封装一次

functionthrottle(fn, delay){vartimer =null;returnfunction(){   
clearTimeout(timer);    timer = setTimeout(function(){      fn();    },
delay); };};

调用

varfunc =
throttle(show,100);functionshow(){console.log(1);}window.onscroll
=function(){  func();}

封装2

functionthrottle(fn, delay, runDelay){vartimer
=null;vart_start;returnfunction(){vart_cur =newDate();    timer &&
clearTimeout(timer);if(!t_start) {      t_start = t_cur;   
}if(t_cur – t_start >= runDelay) {      fn();      t_start =
t_cur;    }else{      timer = setTimeout(function(){        fn();     
}, delay);    }  }}

调用

varfunc =
throttle(show,50,100);functionshow(){console.log(1);}window.onscroll
=function(){  func();}

函数去抖的实现:

代码在underscore的基础上进行了扩充

// 函数去抖(连续事件触发结束后只触发一次)// sample 1:
_.debounce(function(){}, 1000)// 连续事件结束后的 1000ms 后触发//
sample 1: _.debounce(function(){}, 1000, true)//
连续事件触发后立即触发(此时会忽略第二个参数)_.debounce
=function(func, wait, immediate){vartimeout, args, context, timestamp,
result;varlater =function(){// 定时器设置的回调 later
方法的触发时间,和连续事件触发的最后一次时间戳的间隔 // 如果间隔为
wait(或者刚好大于 wait),则触发事件 varlast = _.now() – timestamp;//
时间间隔 last 在 [0, wait) 中 // 还没到触发的点,则继续设置定时器 //
last 值应该不会小于 0 吧? if(last < wait && last >=0) {     
timeout = setTimeout(later, wait – last);    }else{//
到了可以触发的时间点 timeout = null; // 可以触发了 //
并且不是设置为立即触发的 //
因为如果是立即触发(callNow),也会进入这个回调中 // 主要是为了将
timeout 值置为空,使之不影响下次连续事件的触发//
如果不是立即执行,随即执行 func 方法 if(!immediate) {// 执行 func 函数
result = func.apply(context, args);// 这里的 timeout 一定是 null 了吧 //
感觉这个判断多余了 if(!timeout)            context = args =null;       
}      }    };// 嗯,闭包返回的函数,是可以传入参数的
returnfunction(){// 可以指定 this 指向 context =this;    args
=arguments;// 每次触发函数,更新时间戳 // later 方法中取 last
值时用到该变量 // 判断距离上次触发事件是否已经过了 wait seconds 了 //
即我们需要距离最后一次触发事件 wait seconds 后触发这个回调方法timestamp
= _.now();// 立即触发需要满足两个条件 // immediate 参数为 true,并且
timeout 还没设置 // immediate 参数为 true 是显而易见的 // 如果去掉
!timeout 的条件,就会一直触发,而不是触发一次 //
因为第一次触发后已经设置了 timeout,所以根据 timeout
是否为空可以判断是否是首次触发 varcallNow = immediate && !timeout;//
设置 wait seconds 后触发 later 方法 // 无论是否 callNow(如果是
callNow,也进入 later 方法,去 later 方法中判断是否执行相应回调函数) //
在某一段的连续触发中,只会在第一次触发时进入这个 if 分支中
if(!timeout)// 设置了 timeout,所以以后不会进入这个 if 分支了 timeout =
setTimeout(later, wait);// 如果是立即触发 if(callNow) {// func
可能是有返回值的 result = func.apply(context, args);// 解除引用 context
= args =null;    }returnresult;  };};

节流函数

varthrottle =function(func, wait){vartimeout, context, args, startTime
=Date.parse(newDate());returnfunction(){varcurTime
=Date.parse(newDate());varremaining = wait – (curTime – startTime);
context =this; args =arguments; clearTimeout(timeout);if(remaining
<=0){ func.apply(context, args); startTime =Date.parse(newDate());
}else{ timeout = setTimeout(func, remaining); } }};

链接:

//节流函数(连续触发会不执行)

    // throttle:function (func, wait){

    //    var timeout,

    //        context,

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图