详解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 相关文章推荐
用JavaScript实现单继承和多继承的简单方法
Mar 29 Javascript
关于viewport,Ext.panel和Ext.form.panel的关系
May 07 Javascript
js中的string.format函数代码
Aug 11 Javascript
js中将URL中的参数提取出来作为对象的实现代码
Aug 16 Javascript
cocos2dx骨骼动画Armature源码剖析(三)
Sep 08 Javascript
JS实现的竖向折叠菜单代码
Oct 21 Javascript
jquery购物车结算功能实现方法
Oct 29 Javascript
angularJS深拷贝详解
Mar 23 Javascript
node打造微信个人号机器人的方法示例
Apr 26 Javascript
Node.js Express安装与使用教程
May 11 Javascript
对vuex中getters计算过滤操作详解
Nov 06 Javascript
vantUI 获得piker选中值的自定义ID操作
Nov 04 Javascript
很棒的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
php 获取客户端的真实ip
2009/11/30 PHP
PHP 命令行参数详解及应用
2011/05/18 PHP
PHP微信公众号自动发送红包API
2016/06/01 PHP
php中的登陆login实例代码
2016/06/20 PHP
wordpress网站转移到本地运行测试的方法
2017/03/15 PHP
Laravel程序架构设计思路之使用动作类
2018/06/07 PHP
jquery 1.3.2 IE8中的一点点的小问题解决方法
2009/07/10 Javascript
基于JQuery的6个Tab选项卡插件
2010/09/03 Javascript
javascript 闭包
2011/09/15 Javascript
简单谈谈javascript中的变量、作用域和内存问题
2015/08/30 Javascript
JS 日期与时间戮相互转化的简单实例
2016/06/22 Javascript
如何防止INPUT按回车自动提交表单FORM
2016/12/06 Javascript
js实现5秒倒计时重新发送短信功能
2017/02/05 Javascript
Angular中使用$watch监听object属性值的变化(详解)
2017/04/24 Javascript
解决vue 按钮多次点击重复提交数据问题
2018/05/10 Javascript
详解Vue组件之作用域插槽
2018/11/22 Javascript
微信小程序开发注意指南和优化实践(小结)
2019/06/21 Javascript
JSX在render函数中的应用详解
2019/09/04 Javascript
小程序实现投票进度条
2019/11/20 Javascript
使用npm命令提示: 'npm' 不是内部或外部命令,也不是可运行的程序的处理方法
2020/05/14 Javascript
通过实例解析JavaScript for in及for of区别
2020/06/15 Javascript
jquery实现异步文件上传ajaxfileupload.js
2020/10/23 jQuery
[48:46]完美世界DOTA2联赛PWL S2 SZ vs FTD.C 第二场 11.19
2020/11/19 DOTA
用python写个自动SSH登录远程服务器的小工具(实例)
2017/06/17 Python
基于python 处理中文路径的终极解决方法
2018/04/12 Python
python同步两个文件夹下的内容
2019/08/29 Python
Django 限制访问频率的思路详解
2019/12/24 Python
Python爬虫爬取百度搜索内容代码实例
2020/06/05 Python
维多利亚的秘密官方旗舰店:VICTORIA’S SECRET
2018/04/02 全球购物
大学生活动总结怎么写
2014/04/29 职场文书
教育专业毕业生推荐信
2014/07/10 职场文书
毕业生实习证明
2014/09/19 职场文书
消防演习通知
2015/04/25 职场文书
Python Pandas模块实现数据的统计分析的方法
2021/06/24 Python
Python中的matplotlib绘制百分比堆叠柱状图,并为每一个类别设置不同的填充图案
2022/04/20 Python
Nginx 配置 HTTPS的详细过程
2022/05/30 Servers