JavaScript 节流函数 Throttle 详解


Posted in Javascript onJuly 04, 2016

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

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

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

var COUNT = 0;
function testFn() { console.log(COUNT++); }
// 浏览器resize的时候
// 1. 清除之前的计时器
// 2. 添加一个计时器让真正的函数testFn延后100毫秒触发
window.onresize = function () {
 var timer = null;
 clearTimeout(timer);

 timer = setTimeout(function() {
  testFn();
 }, 100);
};

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

于是我们修改了代码:

var timer = null;
window.onresize = function () {
 clearTimeout(timer);
 timer = setTimeout(function() {
  testFn();
 }, 100);
};

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

/**
 * 函数节流方法
 * @param Function fn 延时调用函数
 * @param Number delay 延迟多长时间
 * @return Function 延迟执行的方法
 */
var throttle = function (fn, delay) {
 var timer = null;

 return function () {
  clearTimeout(timer);
  timer = setTimeout(function() {
   fn();
  }, delay);
 }
};
window.onresize = throttle(testFn, 200, 1000);

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

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

var throttle = function (fn, delay) {
 var timer = null;

 return function () {
  clearTimeout(timer);
  timer = setTimeout(function() {
   fn();
  }, delay);
 }
};

var f = throttle(testFn, 200);
window.onresize = function () {
 f();
};

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

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

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

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

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

/**
 * 函数节流方法
 * @param Function fn 延时调用函数
 * @param Number delay 延迟多长时间
 * @param Number atleast 至少多长时间触发一次
 * @return Function 延迟执行的方法
 */
var throttle = function (fn, delay, atleast) {
 var timer = null;
 var previous = null;

 return function () {
  var now = +new Date();

  if ( !previous ) previous = now;

  if ( now - previous > atleast ) {
   fn();
   // 重置上一次开始时间为本次结束时间
   previous = now;
  } else {
   clearTimeout(timer);
   timer = setTimeout(function() {
    fn();
   }, delay);
  }
 }
};

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

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>throttle</title>
</head>
<body>
 <div style="height:5000px">
  <div id="demo" style="position:fixed;"></div>
 </div>
 <script>
 var COUNT = 0, demo = document.getElementById('demo');
 function testFn() {demo.innerHTML += 'testFN 被调用了 ' + ++COUNT + '次<br>';}

 var throttle = function (fn, delay, atleast) {
  var timer = null;
  var previous = null;

  return function () {
   var now = +new Date();

   if ( !previous ) previous = now;
   if ( atleast && now - previous > atleast ) {
    fn();
    // 重置上一次开始时间为本次结束时间
    previous = now;
    clearTimeout(timer);
   } else {
    clearTimeout(timer);
    timer = setTimeout(function() {
     fn();
     previous = null;
    }, delay);
   }
  }
 };
 window.onscroll = throttle(testFn, 200);
 // window.onscroll = throttle(testFn, 500, 1000);
 </script>
</body>
</html>

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

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

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

JavaScript 节流函数 Throttle 详解

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

 JavaScript 节流函数 Throttle 详解

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS 巧妙获取剪贴板数据 Excel数据的粘贴
Jul 09 Javascript
JavaScript 语言的递归编程
May 18 Javascript
JavaScript高级程序设计 客户端存储学习笔记
Sep 10 Javascript
jQuery:delegate中select()不起作用的解决方法(实例讲解)
Jan 26 Javascript
window.location不跳转的问题解决方法
Apr 17 Javascript
AspNet中使用JQuery boxy插件的确认框
May 20 Javascript
jQuery设置Cookie及删除Cookie实例分析
Apr 15 Javascript
JS获取input file绝对路径的方法(推荐)
Aug 02 Javascript
jQuery弹出窗口打开链接的实现代码
Dec 24 Javascript
详解vue-cli官方脚手架配置
Jul 20 Javascript
从Vuex中取出数组赋值给新的数组,新数组push时报错的解决方法
Sep 18 Javascript
vue+webpack 更换主题N种方案优劣分析
Oct 28 Javascript
jQuery实现订单提交页发送短信功能前端处理方法
Jul 04 #Javascript
实用jquery操作表单元素的简单代码
Jul 04 #Javascript
jQuery实现左侧导航模块的显示与隐藏效果
Jul 04 #Javascript
将List对象列表转换成JSON格式的类实现方法
Jul 04 #Javascript
jQuery解析与处理服务器端返回xml格式数据的方法详解
Jul 04 #Javascript
jQuery ajaxSubmit 实现ajax提交表单局部刷新
Jul 04 #Javascript
jQuery通用的全局遍历方法$.each()用法实例
Jul 04 #Javascript
You might like
php中的常用魔术方法汇总
2016/02/14 PHP
PHP访问数据库集群的方法小结
2016/03/14 PHP
PHP实现的策略模式简单示例
2017/08/25 PHP
PHP中cookie知识点学习
2018/05/06 PHP
在JavaScript中实现类的方式探讨
2013/08/28 Javascript
jQuery-ui引入后Vs2008的无智能提示问题解决方法
2014/02/10 Javascript
JavaScript对象的property属性详解
2014/04/01 Javascript
jquery实现从数组移除指定的值
2015/06/24 Javascript
JavaScript严格模式详解
2015/11/18 Javascript
JQuery为元素添加样式的实现方法
2016/07/20 Javascript
微信小程序 生命周期详解
2016/10/12 Javascript
angular十大常见问题
2017/03/07 Javascript
ES6生成器用法实例分析
2017/04/10 Javascript
React Native 使用Fetch发送网络请求的示例代码
2017/12/02 Javascript
Vue表情输入组件 微信face表情组件
2019/02/11 Javascript
基于layui table返回的值的多级嵌套的解决方法
2019/09/19 Javascript
微信小程序网络请求实现过程解析
2019/11/06 Javascript
Vue父子之间值传递的实例教程
2020/07/02 Javascript
[03:49]DOTA2 2015国际邀请赛中国区预选赛第二日现场百态
2015/05/27 DOTA
Python中输出ASCII大文字、艺术字、字符字小技巧
2015/04/28 Python
Python使用Dijkstra算法实现求解图中最短路径距离问题详解
2018/05/16 Python
Python中使用logging和traceback模块记录日志和跟踪异常
2019/04/09 Python
PyQt5图形界面播放音乐的实例
2019/06/17 Python
Python jieba库用法及实例解析
2019/11/04 Python
在pycharm中为项目导入anacodna环境的操作方法
2020/02/12 Python
详解numpy.ndarray.reshape()函数的参数问题
2020/10/13 Python
Python爬取你好李焕英豆瓣短评生成词云的示例代码
2021/02/24 Python
HTML5 audio标签使用js进行播放控制实例
2015/04/24 HTML / CSS
int *p=NULL和*p= NULL有什么区别
2014/10/23 面试题
白酒业务员岗位职责
2013/12/27 职场文书
幼儿园运动会口号
2014/06/07 职场文书
2014年学习厉行节约反对浪费思想汇报
2014/09/10 职场文书
社区六一儿童节活动总结
2015/02/11 职场文书
家属联谊会致辞
2015/07/31 职场文书
晚会开幕词范文
2016/03/04 职场文书
Java实现斗地主之洗牌发牌
2021/06/14 Java/Android