图片 2

浅谈javascript函数节流

浅谈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);
}

执行截图效果如下:

图片 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 收藏
评论

图片 2

函数节流场景

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

函数去抖场景

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

函数节流的实现

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

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); } }};

链接:https://www.jianshu.com/p/a084bbe9102c

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

    // throttle:function (func, wait){

    //    var timeout,

    //        context,

    //        args,

    //        startTime = Date.parse(new Date());

    //

    //    return function(){

    //        var curTime = Date.parse(new Date());

    //        var remaining = wait – (curTime – startTime);

    //        context = this;

    //        args = arguments;

    //

    //        clearTimeout(timeout);

    //

    //        if(remaining <= 0){

    //            func.apply(context, args);

    //            startTime = Date.parse(new Date());

    //        }else

    //            timeout = setTimeout(func, remaining);

    //        }

    //    }

    // },

   
//delay的间隔内连续触发的调用,后一个调用会把前一个调用的等待处理掉,但每隔mustRunDelay至少执行一次。第2个版本,其实是防抖

    // throttle :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);

    //        }

    //    }

    // },

    //防抖

    // debounce:function (func, wait, immediate) {

    //    var timeout;

    //    return function() {

    //        var context = this, args = arguments;

    //        var later = function() {

    //            timeout = null;

    //            if (!immediate) func.apply(context, args);

    //        };

    //        var callNow = immediate && !timeout;

    //        clearTimeout(timeout);

    //        timeout = setTimeout(later, wait);

    //        if (callNow) func.apply(context, args);

    //    };

    // },