详解JavaScript节流函数中的Throttle


Posted in Javascript onJuly 16, 2016

首先我们来了解下什么是Throttle 

    1. 定义

如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。

也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。

      接口定义:

* 频率控制 返回函数连续调用时,action 执行频率限定为 次 / delay
* @param delay {number} 延迟时间,单位毫秒
* @param action {function} 请求关联函数,实际应用需要调用的函数
* @return {function} 返回客户调用函数
*/
throttle(delay,action)

    2. 简单实现

var throttle = function(delay, action){
 var last = 0return function(){
 var curr = +new Date()
 if (curr - last > delay){
  action.apply(this, arguments)
  last = curr 
 }
 }
}

下面我仔细解释一下这个节流函数。

在浏览器 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 的延迟逻辑。

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

previous 变量的作用和 timer 类似,都是记录上一次的标识,必须是相对的全局变量
如果逻辑流程走的是“至少触发一次”的逻辑,那么函数调用完成需要把 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 ,效果如图:

 详解JavaScript节流函数中的Throttle

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

 详解JavaScript节流函数中的Throttle

如上展示,我们要实现的效果已经介绍完毕并奉上了示例,望对有需要的朋友有所帮助。后续的一些辅助性优化读者可以自己琢磨,如:函数 this 指向,返回值保存等。总之仔仔细细理解一下这个过程感觉真好!

Javascript 相关文章推荐
自动检查并替换文本框内的字符
Jun 30 Javascript
jquery动画2.元素坐标动画效果(创建一个图片走廊)
Aug 24 Javascript
javascript判断office版本示例
Apr 11 Javascript
IE6-8中Date不支持toISOString的修复方法
May 04 Javascript
js基于setTimeout与setInterval实现多线程
Jun 17 Javascript
JavaScript实现事件的中断传播和行为阻止方法示例
Jan 20 Javascript
js实现短信发送倒计时功能(正则验证)
Feb 10 Javascript
微信小程序自定义导航隐藏和显示功能
Jun 13 Javascript
bootstrap轮播模板使用方法详解
Nov 17 Javascript
JS实现的简单分页功能示例
Aug 23 Javascript
JS数组降维的实现Array.prototype.concat.apply([], arr)
Apr 28 Javascript
Vue使用Element实现增删改查+打包的步骤
Nov 25 Vue.js
很棒的js选项卡切换效果
Jul 15 #Javascript
轻松5句话解决JavaScript的作用域
Jul 15 #Javascript
jQuery EasyUI基础教程之EasyUI常用组件(推荐)
Jul 15 #Javascript
IE下JS保存图片的简单实例
Jul 15 #Javascript
jQuery 3.0中存在问题及解决办法
Jul 15 #Javascript
JavaScript6 let 新语法优势介绍
Jul 15 #Javascript
简单实现轮播图效果的实例
Jul 15 #Javascript
You might like
咖啡知识 除了喝咖啡还有那些知识点
2021/03/06 新手入门
提升PHP速度全攻略
2006/10/09 PHP
PHP4实际应用经验篇(4)
2006/10/09 PHP
php UNIX时间戳用法详解
2017/02/16 PHP
PHP给前端返回一个JSON对象的实例讲解
2018/05/31 PHP
jquery左右滚动焦点图banner图片鼠标经过显示上下页按钮
2013/10/11 Javascript
jquery选择器排除某个DOM元素的方法(实例演示)
2014/04/25 Javascript
浅析webapp框架AngularUI的demo
2014/12/21 Javascript
Js与Jq获取浏览器和对象值的方法
2016/03/18 Javascript
jQuery fadeOut 异步实例代码详解
2016/08/18 Javascript
bootstrap手风琴制作方法详解
2017/01/11 Javascript
Vue.js展示AJAX数据简单示例讲解
2017/03/29 Javascript
Vue.js教程之axios与网络传输的学习实践
2017/04/29 Javascript
详解Vue2.0里过滤器容易踩到的坑
2017/06/01 Javascript
深入讲解xhr(XMLHttpRequest)/jsonp请求之abort
2017/07/26 Javascript
js实现图片轮播效果学习笔记
2017/07/26 Javascript
微信小程序分享功能之按钮button 边框隐藏和点击隐藏
2018/06/14 Javascript
微信小程序倒计时功能实例代码
2018/07/17 Javascript
mpvue将vue项目转换为小程序
2018/09/30 Javascript
vue v-for出来的列表,点击某个li使得当前被点击的li字体变红操作
2020/07/17 Javascript
Element Input输入框的使用方法
2020/07/26 Javascript
微信小程序实现左滑删除效果
2020/11/18 Javascript
Python里隐藏的“禅”
2014/06/16 Python
Python的re模块正则表达式操作
2016/05/25 Python
python中for循环输出列表索引与对应的值方法
2018/11/07 Python
pytorch中tensor.expand()和tensor.expand_as()函数详解
2019/12/27 Python
CSS3实现复选框动画特效示例代码
2016/09/27 HTML / CSS
浅谈html5增强的页面元素
2016/06/14 HTML / CSS
荷兰皇家航空公司官方网站:KLM Royal Dutch Airlines
2017/12/07 全球购物
秋季运动会活动方案
2014/02/05 职场文书
五好党支部事迹材料
2014/02/06 职场文书
会计学专业自荐信
2014/06/25 职场文书
2014年安全生产工作总结
2014/11/13 职场文书
2014年小学数学工作总结
2014/12/12 职场文书
联谊会开场白
2015/06/01 职场文书
员工安全责任协议书
2016/03/22 职场文书