金沙棋牌官方平台

当前位置:金沙棋牌 > 金沙棋牌官方平台 > 变动并高效响应,浅谈javascript函数节流

变动并高效响应,浅谈javascript函数节流

来源:http://www.logblo.com 作者:金沙棋牌 时间:2019-11-22 11:09

如何监听页面 DOM 变动并高效响应

2017/08/04 · JavaScript · DOM

原文出处: hijiangtao   

最近在做 chrome 插件开发,既然是插件那就难免不对现有页面做一些控制,比如事件监听、调整布局、对 DOM 元素的增删改查等等。其中有一个需求比较有意思,便整理一下顺便把涉及到的知识点复习一遍。

需求是这样的:在一个包含懒加载资源以及动态 DOM 元素生成的页面中,需要针对页面中存在的元素添加属性显示标签。

函数节流场景

什么是函数节流?

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

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

金沙棋牌官方平台 1

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

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

 

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

执行截图效果如下:

金沙棋牌官方平台 2

如上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 收藏 评论

金沙棋牌官方平台 3

从 DOM 变动事件监听说起

首先假设大家已经知道 JavaScript 中事件的发生阶段(捕获-命中-冒泡),附上一张图带过这个内容,我们直接进入寻找解决方法的过程。

金沙棋牌官方平台 4

Graphical representation of an event dispatched in a DOM tree using the DOM event flow

开始的时候我一直在 window 状态改变涉及到的事件中寻找,一圈搜寻下来发现也就 onload 事件最接近了,所以我们看看 MDN 对该事件的定义:

The load event is fired when a resource and its dependent resources have finished loading.

怎么理解资源及其依赖资源已加载完毕呢?简单来说,如果一个页面涉及到图片资源,那么 onload 事件会在页面完全载入(包括图片、css文件等等)后触发。一个简单的监听事件用 JavaScript 应该这样书写(注意不同环境下 load 和 onload 的差异):

<script> window.addEventListener("load", function(event) { console.log("All resources finished loading!"); }); // or window.onload=function(){ console.log("All resources finished loading!"); }; // HTML < body onload="SomeJavaScriptCode"> // jQuery $( window ).on( "load", handler ) </script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
  window.addEventListener("load", function(event) {
    console.log("All resources finished loading!");
  });
  
  // or
  window.onload=function(){
    console.log("All resources finished loading!");
  };
  
  // HTML
< body onload="SomeJavaScriptCode">
  
  // jQuery
  $( window ).on( "load", handler )
</script>

当然,说到 onload 事件,有一个 jQuery 中相似的事件一定会被提及—— ready 事件。jQuery 中这样定义这个事件:

Specify a function to execute when the DOM is fully loaded.

需要知道的是 jQuery 定义的 ready 事件实质上是为 DOMContentLoaded 事件设计的,所以当我们谈论加载时应该区分的事件其实是 onload(接口 UIEvent) 以及 DOMContentLoaded(接口 Event),MDN 这样描述 DOMContentLoaded

当初始HTML文档被完全加载和解析时,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架完成加载。另一个不同的事件 load 应该仅用于检测一个完全加载的页面。

所以可以知道,当一个页面加载时应先触发 DOMContentLoaded 然后才是 onload. 类似的事件及区别包括以下几类:

  • DOMContentLoaded: 当初始HTML文档被完全加载和解析时,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架完成加载;
  • readystatechange: 一个document 的 Document.readyState 属性描述了文档的加载状态,当这个状态发生了变化,就会触发该事件;
  • load: 当一个资源及其依赖资源已完成加载时,将触发load事件;
  • beforeunload: 当浏览器窗口,文档或其资源将要卸载时,会触发beforeunload事件。
  • unload: 当文档或一个子资源正在被卸载时, 触发 unload事件。

细心点会发现上面在介绍事件时提到了 UIEvent 以及 Event,这是什么呢?这些都是事件——可以被 JavaScript 侦测到的行为。其他的事件接口还包括 KeyboardEvent / VRDisplayEvent (是的,没错,这就是你感兴趣且熟知的那个 VR)等等;如果在搜索引擎中稍加搜索,你会发现有些资料里写到事件可以分为以下几类:

  • UI事件
  • 焦点事件
  • 鼠标与滚轮事件
  • 键盘与文本事件
  • 复合事件
  • 变动事件
  • HTML5 事件
  • 设备事件
  • 触摸与手势事件

但这样写实在有些凌乱,其中一些是 DOM3 定义的事件,有一些是单独列出的事件,如果你觉得熟悉那么你会发现这是 JavaScript 高级程序设计里的叙述模式,在我看来,理解这些事件可以按照 DOM3 事件以及其他事件来做区分:其中,DOM3 级事件规定了以下几类事件 – UI 事件, 焦点事件, 鼠标事件, 滚轮事件, 文本事件, 键盘事件, 合成事件, 变动事件, 变动名称事件; 而剩下的例如 HTML5 事件可以单独做了解。而刚开始提到的 Event 作为一个主要接口,是很多事件的实现父类。有关 Web API 接口可以在这里查到,里面可以看到有很多 Event 字眼。

好吧,事件说了这么多,我们还是没有解决刚开始提出的问题,如果监听页面中动态生成的元素呢?想到动态生成的元素都是需要通过网络请求获取资源的,那么是否可以监听所有 HTTP 请求呢?查看 jQuery 文档可以知道每当一个Ajax请求完成,jQuery 就会触发 ajaxComplete 事件,在这个时间点所有处理函数会使用 .ajaxComplete() 方法注册并执行。但是谁能保证所有 ajax 都从 jQuery 走呢?所以应该在变动事件中做出选择,我们来看看 DOM2 定义的如下变动事件:

  • DOMSubtreeModified: 在DOM结构发生任何变化的时候。这个事件在其他事件触发后都会触发;
  • DOMNodeInserted: 当一个节点作为子节点被插入到另一个节点中时触发;
  • DOMNodeRemoved: 在节点从其父节点中移除时触发;
  • DOMNodeInsertedIntoDocument: 在一个节点被直接插入文档或通过子树间接插入文档之后触发。这个事件在 DOMNodeInserted 之后触发;
  • DOMNodeRemovedFromDocument金沙棋牌官方平台,: 在一个节点被直接从文档移除或通过子树间接从文档移除之前触发。这个事件在 DOMNodeRemoved 之后触发;
  • DOMAttrModified: 在特性被修改之后触发;
  • DOMCharacterDataModified: 在文本节点的值发生变化时触发;

所以,用 DOMSubtreeModified 好像没错。师兄旁边提醒,用 MutationObserver, 于是又搜到了一个新大陆。MDN 这样描述 MutationObserver:

MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.

DOM3 事件规范中的 Mutation 事件可以被简单看成是 DOM2 事件规范中定义的 Mutation 事件的一个扩展,但是这些都不重要了,因为他们都要被 MutationObserver 替代了。好了,那么来详细介绍一下 MutationObserver 吧。文章《Mutation Observer API》对 MutationObserver 的用法介绍的比较详细,所以我挑几点能直接解决我们需求的说一说。

既然要监听 DOM 的变化,我们来看看 Observer 的作用都有哪些:

它等待所有脚本任务完成后,才会运行,即采用异步方式。

它把 DOM 变动记录封装成一个数组进行处理,而不是一条条地个别处理 DOM 变动。

它既可以观察发生在 DOM 的所有类型变动,也可以观察某一类变动。

MutationObserver 的构造函数比较简单,传入一个回调函数即可(回调函数接受两个参数,第一个是变动数组,第二个是观察器实例):

let observer = new MutationObserver(callback);

1
let observer = new MutationObserver(callback);

观察器实例使用 observe 方法来监听, disconnect 方法停止监听,takeRecords 方法来清除变动记录。

let article = document.body; let options = { 'childList': true, 'attributes':true } ; observer.observe(article, options);

1
2
3
4
5
6
7
8
let article = document.body;
 
let  options = {
  'childList': true,
  'attributes':true
} ;
 
observer.observe(article, options);

observe 方法中第一个参数是所要观察的变动 DOM 元素,第二个参数则接收所要观察的变动类型(子节点变动和属性变动)。变动类型包括以下几种:

  • childList:子节点的变动。
  • attributes:属性的变动。
  • characterData:节点内容或节点文本的变动。
  • subtree:所有后代节点的变动。

想要观察哪一种变动类型,就在 option 对象中指定它的值为 true。需要注意的是,如果设置观察 subtree 的变动,必须同时指定 childList、attributes 和 characterData 中的一种或多种。disconnect 方法和 takeRecords 方法则直接调用即可,无传入参数。

好的,我们已经搞定了 DOM 变动的监听,将代码刷新一下看下效果吧,因为页面由很多动态生成的商品组成,那么我应该在 body 上添加变动监听,所以 options 应该这样设置:

var options = { 'attributes': true, 'subtree': true }

1
2
3
4
5
var options = {
'attributes': true,
'subtree': true
}
 

咦?页面往下拉一小点就触发了 observer 几十次?这样 DOM 哪吃得消啊,查看了页面的变动记录发现每次新进的资源底层都调用了 Node.insertBefore() 方法…

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

函数节流的原理

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

 

再聊聊 JavaScript 中的截流/节流函数

现在遇到的一个麻烦是, DOM 变动太频繁了,如果每次变动都监听那真是太耗费资源了。一个简单的解决办法是我就放弃监听了,而采用 setInterval 方法定时执行更新逻辑。是的,虽然方法原始了一点,但是性能上比 Observer “改进”了不少。

这个时候,又来了师兄的助攻:“用用截流函数”。记起之前看《JavaScript 语言精粹》的时候看到是用 setTimeout 方法自调用来解决 setInteval 的频繁执行吃资源的现象,不知道两者是不是有关联。网上一查发现有两个“jie流函数”。需求来自于这里:

在前端开发中,页面有时会绑定scroll或resize事件等频繁触发的事件,也就意味着在正常的操作之内,会多次调用绑定的程序,然而有些时候javascript需要处理的事情特别多,频繁出发就会导致性能下降、成页面卡顿甚至是浏览器奔溃。

如果重复利用 setTimeout 和 clearTimeout 方法,我们好像可以解决这个频繁触发的执行。每次事件触发的时候我首先判断一下当前有没有一个 setTimeout 定时器,如果有的话我们先将它清除,然后再新建一个 setTimeout 定时器来延迟我的响应行为。这样听上去还不错,因为我们每次都不立即执行我们的响应,而频繁触发过程我们又能保持响应函数一直存在(且只存在一个),除了会有些延迟响应外,没什么不好的。是的这就是截流函数(debounce),有一篇博客用这个小故事介绍它:

形像的比喻是橡皮球。如果手指按住橡皮球不放,它就一直受力,不能反弹起来,直到松手。debounce 的关注点是空闲的间隔时间。

在我的业务中,在 observer 实例中调用下面写的这个截流函数就可以啦

/** * fn 执行函数 * context 绑定上下文 * timeout 延时数值 **/ let debounce = function(fn, context, timeout) { let timer; // 利用闭包将内容传递出去 return function() { if (timer) { // 清除定时器 clearTimeout(timer); } // 设置一个新的定时器 timer = setTimeout(function(){ fn.apply(context, arguments) }, timeout); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* fn 执行函数
* context 绑定上下文
* timeout 延时数值
**/
let debounce = function(fn, context, timeout) {
let timer;
    
    // 利用闭包将内容传递出去
return function() {
if (timer) {
    // 清除定时器
clearTimeout(timer);
}
// 设置一个新的定时器
timer = setTimeout(function(){
fn.apply(context, arguments)
}, timeout);
}
}

当然,解决了自己的问题,但还有一个概念没有说到——“节流函数”。同一篇博文里也使用了一个例子来说明它:

形像的比喻是水龙头或机枪,你可以控制它的流量或频率。throttle 的关注点是连续的执行间隔时间。

函数节流的原理也挺简单,一样还是定时器。当我触发一个时间时,先setTimout让这个事件延迟一会再执行,如果在这个时间间隔内又触发了事件,那我们就清除原来的定时器,再setTimeout一个新的定时器延迟一会执行。函数节流的出发点,就是让一个函数不要执行得太频繁,减少一些过快的调用来节流。这里引用 AlloyTeam 的节流代码实现来解释:

// 参数同上 var 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); } }; };

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
// 参数同上
var 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);
}
};
};

当然,AlloyTeam 那篇文章将这里所说的截流函数作为节流函数的 V1.0 版本,你也可以这样认为。毕竟,设置了必然触发执行的时间间隔(即 mustRunDelay 函数),可以使得截流函数不会在“疯狂事件”情况下无止境的循环下去。

Observer 和截流函数一结合,问题解决啦嘿嘿。当然还有很多坑,下次再开一篇说说吧。

函数去抖场景

代码实现

明白了原理,那就可以在代码里用上了,但每次都要手动去新建清除定时器毕竟麻烦,于是需要封装。在《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上下文传给执行函数,但毕竟不够灵活。

 

参考

  • 1 赞 3 收藏 评论

金沙棋牌官方平台 5

例如:对于浏览器窗口,每做一次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的性能优化得太好了,我的第一种测试方案三种方法之间有性能差异,但这个差异实在不明显,而且每一轮的测试都有波动,而且每次测试还很难保证测试的背景条件(如开始时的内存占用情况),第一组测试结果如下:

第一种方法:金沙棋牌官方平台 6

第二种方法:金沙棋牌官方平台 7

第三种方法:金沙棋牌官方平台 8

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

 

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

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

第一种方法:金沙棋牌官方平台 9

第二种方法:金沙棋牌官方平台 10

第三种方法:金沙棋牌官方平台 11

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

仔细分析。第一种方法由于不断地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,

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

    //    };

    // },

本文由金沙棋牌发布于金沙棋牌官方平台,转载请注明出处:变动并高效响应,浅谈javascript函数节流

关键词:

上一篇:没有了

下一篇:没有了