开发者社区> 行者武松> 正文

JavaScript 节流函数 Throttle 详解

简介:
+关注继续查看

在浏览器 DOM 事件里面,有一些事件会随着用户的操作不间断触发。比如:重新调整浏览器窗口大小(resize),浏览器页面滚动(scroll),鼠标移动 (mousemove)。也就是说用户在触发这些浏览器操作的时候,如果脚本里面绑定了对应的事件处理方法,这个方法就不停的触发。

这并不是我们想要的,因为有的时候如果事件处理方法比较庞大,DOM 操作比如复杂,还不断的触发此类事件就会造成性能上的损失,导致用户体验下降(UI 反映慢、浏览器卡死等)。所以通常来讲我们会给相应事件添加延迟执行的逻辑。

通常来说我们用下面的代码来实现这个功能:


  1. var COUNT = 0; 
  2. function testFn() { console.log(COUNT++); } 
  3. // 浏览器resize的时候 
  4. // 1. 清除之前的计时器 
  5. // 2. 添加一个计时器让真正的函数testFn延后100毫秒触发 
  6. window.onresize = function () { 
  7.     var timer = null
  8.     clearTimeout(timer); 
  9.  
  10.     timer = setTimeout(function() { 
  11.         testFn(); 
  12.     }, 100); 
  13. }; 

细心的同学会发现上面的代码其实是错误的,这是新手会犯的一个问题:setTimeout 函数返回值应该保存在一个相对全局变量里面,否则每次 resize 的时候都会产生一个新的计时器,这样就达不到我们发的效果了

于是我们修改了代码:


  1. var timer = null
  2. window.onresize = function () { 
  3.     clearTimeout(timer); 
  4.     timer = setTimeout(function() { 
  5.         testFn(); 
  6.     }, 100); 
  7. }; 

这时候代码就正常了,但是又多了一个新问题 —— 产生了一个全局变量 timer。这是我们不想见到的,如果这个页面还有别的功能也叫 timer 不同的代码之前就是产生冲突。为了解决这个问题我们要用 JavaScript 的一个语言特性:闭包 closures 。相关知识读者可以去 MDN 中了解,改造后的代码如下:


  1. /** 
  2.  * 函数节流方法 
  3.  * @param Function fn 延时调用函数 
  4.  * @param Number delay 延迟多长时间 
  5.  * @return Function 延迟执行的方法 
  6.  */ 
  7. var throttle = function (fn, delay) { 
  8.     var timer = null
  9.  
  10.     return function () { 
  11.         clearTimeout(timer); 
  12.         timer = setTimeout(function() { 
  13.             fn(); 
  14.         }, delay); 
  15.     } 
  16. }; 
  17. window.onresize = throttle(testFn, 200, 1000); 

我们用一个闭包函数(throttle节流)把 timer 放在内部并且返回延时处理函数,这样以来 timer 变量对外是不可见的,但是内部延时函数触发时还可以访问到 timer 变量。

当然这种写法对于新手来说不好理解,我们可以变换一种写法来理解一下:


  1. var throttle = function (fn, delay) { 
  2.     var timer = null
  3.  
  4.     return function () { 
  5.         clearTimeout(timer); 
  6.         timer = setTimeout(function() { 
  7.             fn(); 
  8.         }, delay); 
  9.     } 
  10. }; 
  11.  
  12. var f = throttle(testFn, 200); 
  13. window.onresize = function () { 
  14.     f(); 
  15. }; 

这里主要了解一点:throttle 被调用后返回的 function 才是真正的 onresize 触发时需要调用的函数

现在看起来这个方法已经接近完美了,然而实际使用中并非如此。举个例子:

如果用户 不断的 resize 浏览器窗口大小,这时延迟处理函数一次都不会执行

于是我们又要添加一个功能:当用户触发 resize 的时候应该 在某段时间 内至少触发一次,既然是在某段时间内,那么这个判断条件就可以取当前的时间毫秒数,每次函数调用把当前的时间和上一次调用时间相减,然后判断差值如果大于 某段时间 就直接触发,否则还是走 timeout 的延迟逻辑。

下面的代码里面需要指出的是:

  1. previous 变量的作用和 timer 类似,都是记录上一次的标识,必须是相对的全局变量
  2. 如果逻辑流程走的是“至少触发一次”的逻辑,那么函数调用完成需要把 previous 重置成当前时间,简单来说就是:相对于下一次的上一次其实就是当前

  1. /** 
  2.  * 函数节流方法 
  3.  * @param Function fn 延时调用函数 
  4.  * @param Number delay 延迟多长时间 
  5.  * @param Number atleast 至少多长时间触发一次 
  6.  * @return Function 延迟执行的方法 
  7.  */ 
  8. var throttle = function (fn, delay, atleast) { 
  9.     var timer = null
  10.     var previous = null
  11.  
  12.     return function () { 
  13.         var now = +new Date(); 
  14.  
  15.         if ( !previous ) previous = now; 
  16.  
  17.         if ( now - previous > atleast ) { 
  18.             fn(); 
  19.             // 重置上一次开始时间为本次结束时间 
  20.             previous = now; 
  21.         } else { 
  22.             clearTimeout(timer); 
  23.             timer = setTimeout(function() { 
  24.                 fn(); 
  25.             }, delay); 
  26.         } 
  27.     } 
  28. }; 

实践:

我们模拟一个窗口 scroll 时节流的场景,也就是说当用户滚动页面向下的时候我们需要节流执行一些方法,比如:计算 DOM 位置等需要连续操作 DOM 元素的动作

完整代码如下:


  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <title>throttle</title> 
  6. </head> 
  7. <body> 
  8.     <div style="height:5000px"
  9.         <div id="demo" style="position:fixed;"></div> 
  10.     </div> 
  11.     <script> 
  12.     var COUNT = 0, demo = document.getElementById('demo'); 
  13.     function testFn() {demo.innerHTML += 'testFN 被调用了 ' + ++COUNT + '次<br>';} 
  14.  
  15.     var throttle = function (fn, delay, atleast) { 
  16.         var timer = null
  17.         var previous = null
  18.  
  19.         return function () { 
  20.             var now = +new Date(); 
  21.  
  22.             if ( !previous ) previous = now; 
  23.             if ( atleast && now - previous > atleast ) { 
  24.                 fn(); 
  25.                 // 重置上一次开始时间为本次结束时间 
  26.                 previous = now; 
  27.                 clearTimeout(timer); 
  28.             } else { 
  29.                 clearTimeout(timer); 
  30.                 timer = setTimeout(function() { 
  31.                     fn(); 
  32.                     previous = null
  33.                 }, delay); 
  34.             } 
  35.         } 
  36.     }; 
  37.     window.onscroll = throttle(testFn, 200); 
  38.     // window.onscroll = throttle(testFn, 500, 1000); 
  39.     </script> 
  40. </body> 
  41. </html> 

我们用两个 case 来测试效果,分别是添加至少触发 atleast 参数和不添加:


  1. // case 1 
  2. window.onscroll = throttle(testFn, 200); 
  3. // case 2 
  4. window.onscroll = throttle(testFn, 200, 500); 

case 1 的表现为:在页面滚动的过程(不能停止)中 testFN 不会被调用,直到停止的时候会调用一次,也就是说执行的是 throttle 里面 最后 一个 setTimeout ,效果如图(查看原 gif 图):case1

case 2 的表现为:在页面滚动的过程(不能停止)中 testFN 第一次会延迟 500ms 执行(来自至少延迟逻辑),后来至少每隔 500ms 执行一次,效果如图case2

至此为止,我们想要实现的效果已经基本完成。后续的一些辅助性优化读者可以自己琢磨,如:函数 this 指向,返回值保存等。


来源:51CTO

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
函数计算入口参数event详解
函数计算入口参数event是一个可以根据具体需求高度自由化定制的参数,真的可以为所欲为
2952 0
【Boost】boost库asio详解2——io_service::run函数无任务时退出的问题
io_service::work类可以使io_service::run函数在没有任务的时候仍然不返回,直至work对象被销毁。 [cpp] view plain copy    print? void test_asio_nowork()   {       b...
1277 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
27698 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
12962 0
iOS 下APNS推送处理函数详解
相比起Android,iOS在推送方面无疑惯例得更好。APNS(Apple Push Notification Service)是苹果公司提供的消息推送服务。其原理就是,第三方应用将要推送给用户的信息推送到苹果服务器,苹果服务器再通过统一的系统接口将这些信息推送到用户的手机上。如果对此不舍了解的朋友可以参见这篇文章:一步一步教你做ios 推送 本文着重叫在App端如何处理推送信息。主
1426 0
C++函数指针详解
来源:http://www.cnblogs.com/ggjucheng/archive/2011/12/13/2286391.html 指针的概念 指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让我们分别说明。  先声明几
1803 0
malloc函数详解
  malloc的全称是memory allocation,中文叫动态内存分配,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存。下面具体介绍一下malloc函数,,希望帮助大家了解。
753 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
19976 0
+关注
行者武松
杀人者,打虎武松也。
17112
文章
2569
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载