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 相关文章推荐
Jquery 弹出层插件实现代码
Oct 24 Javascript
jquery禁止输入数字以外的字符的示例(纯数字验证码)
Apr 10 Javascript
jquery实现可关闭的倒计时广告特效代码
Sep 02 Javascript
JS实现DIV容器赋值的方法
Dec 14 Javascript
JS中的eval 为什么加括号
Apr 13 Javascript
vue多种弹框的弹出形式的示例代码
Sep 18 Javascript
vue webuploader 文件上传组件开发
Sep 23 Javascript
Vue引用第三方datepicker插件无法监听datepicker输入框的值的解决
Jan 27 Javascript
Bootstrap4如何定制自己的颜色和风格
Feb 26 Javascript
vue中的计算属性实例详解
Sep 19 Javascript
element-ui 远程搜索组件el-select在项目中组件化的实现代码
Dec 04 Javascript
原生JS实现微信通讯录
Jun 18 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中对xml读取的相关函数的介绍一
2008/06/05 PHP
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 2611816 bytes)
2014/11/08 PHP
推荐一款MAC OS X 下php集成开发环境mamp
2014/11/08 PHP
解析 thinkphp 框架中的部分方法
2017/05/07 PHP
IE autocomplete internet explorer's autocomplete
2007/06/30 Javascript
常用一些Javascript判断函数
2012/08/14 Javascript
jQuery使用hide方法隐藏指定元素class样式用法实例
2015/03/30 Javascript
BOM系列第三篇之定时器应用(时钟、倒计时、秒表和闹钟)
2016/08/17 Javascript
jQuery页面弹出框实现文件上传
2017/02/09 Javascript
javascript基础练习之翻转字符串与回文
2017/02/20 Javascript
node.js中debug模块的简单介绍与使用
2017/04/25 Javascript
jquery应用实例分享_实现手风琴特效
2018/02/01 jQuery
使用Vuex实现一个笔记应用的方法
2018/03/13 Javascript
Vue.js 实现微信公众号菜单编辑器功能(一)
2018/05/08 Javascript
Vue项目全局配置页面缓存之按需读取缓存的实现详解
2018/08/01 Javascript
JavaScript实现旋转木马轮播图
2020/03/16 Javascript
解决vue做详情页跳转的时候使用created方法 数据不会更新问题
2020/07/24 Javascript
vue 需求 data中的数据之间的调用操作
2020/08/05 Javascript
[37:02]OG vs INfamous 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/17 DOTA
Python中如何优雅的合并两个字典(dict)方法示例
2017/08/09 Python
Python3实现带附件的定时发送邮件功能
2020/12/22 Python
详解用python写网络爬虫-爬取新浪微博评论
2019/05/10 Python
django框架防止XSS注入的方法分析
2019/06/21 Python
CSS3制作苹果风格键盘特效
2015/02/26 HTML / CSS
使用canvas一步步实现图片打码功能的方法
2019/06/17 HTML / CSS
英国在线药房:Express Chemist
2019/03/28 全球购物
财务会计专业毕业生自荐信
2013/10/19 职场文书
党校毕业心得体会
2014/09/13 职场文书
出差报告范文
2014/11/06 职场文书
买卖合同纠纷代理词
2015/05/25 职场文书
利用html+css实现菜单栏缓慢下拉效果的示例代码
2021/03/30 HTML / CSS
PHP中->和=>的意思
2021/03/31 PHP
MySQL 数据丢失排查案例
2021/05/08 MySQL
Jupyter Notebook内使用argparse报错的解决方案
2021/06/03 Python
MySQL索引是啥?不懂就问
2021/07/21 MySQL
GoFrame框架数据校验之校验结果Error接口对象
2022/06/21 Golang