在浏览器 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 ,效果如图:
 
 
 
case 2 的表现为:在页面滚动的过程(不能停止)中 testFN 第一次会延迟 500ms 执行(来自至少延迟逻辑),后来至少每隔 500ms 执行一次,效果如图
 
 
 
至此为止,我们想要实现的效果已经基本完成。后续的一些辅助性优化读者可以自己琢磨,如:函数 this 指向,返回值保存等。
 

JavaScript 节流函数 Throttle 详解的更多相关文章

  1. javascript some()函数用法详解

    参数说明callback: 要对每个数组元素执行的回调函数.thisObject : 在执行回调函数时定义的this对象. 功能说明对数组中的每个元素都执行一次指定的函数(callback),直到此函 ...

  2. JS中的函数节流throttle详解和优化

    JS中的函数节流throttle详解和优化在前端开发中,有时会为页面绑定resize事件,或者为一个页面元素绑定拖拽事件(mousemove),这种事件有一个特点,在一个正常的操作中,有可能在一个短的 ...

  3. javascript:function 函数声明和函数表达式 详解

    函数声明(缩写为FD)是这样一种函数: 有一个特定的名称 在源码中的位置:要么处于程序级(Program level),要么处于其它函数的主体(FunctionBody)中 在进入上下文阶段创建 影响 ...

  4. (转)javascript中event对象详解

    原文:http://jiajiale.iteye.com/blog/195906 javascript中event对象详解          博客分类: javaScript JavaScriptCS ...

  5. Javascript 异步加载详解

    Javascript 异步加载详解 本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy ...

  6. eval()函数用法详解

    eval()函数用法详解:此函数可能使用的频率并不是太高,但是在某些情况下具有很大的作用,下面就介绍一下eval()函数的用法.语法结构: eval(str) 此函数可以接受一个字符串str作为参数, ...

  7. 【JavaScript中的this详解】

    前言 this用法说难不难,有时候函数调用时,往往会搞不清楚this指向谁?那么,关于this的用法,你知道多少呢? 下面我来给大家整理一下关于this的详细分析,希望对大家有所帮助! this指向的 ...

  8. JavaScript中的this详解

    前言 this用法说难不难,有时候函数调用时,往往会搞不清楚this指向谁?那么,关于this的用法,你知道多少呢? 下面我来给大家整理一下关于this的详细分析,希望对大家有所帮助! this指向的 ...

  9. Javascript中prototype属性详解 (存)

    Javascript中prototype属性详解   在典型的面向对象的语言中,如java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例.但是在Javascript语言体系中,是不 ...

随机推荐

  1. .NET(WinCE、WM)转Android开发——Xamarin和Smobiler对比

    对比 WinCE Android 行业场景 扫描分拣.车载.工控 扫描分拣.车载定位 开发语言 C++.C# Java/.NET(Smobiler) 开发环境 Visual Studio Androi ...

  2. Java设计模式小议之1------- 迭代器模式

    定义:提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的内部细节. 类型:行为类模式 这里用一个具体的案例来说明一下迭代器模式的简单使用 我们都知道在商店中,经常要把商品放到书架上,并将商品的 ...

  3. nginx + lua 限制ip地址访问

    实验环境:docker + openresty 我限制的5秒钟内允许访问两次效果图: default.conf  代码如下: lua_shared_dict my_limit_count_store ...

  4. 入门者必看!SharePoint之CAML总结(实战)

    分享人:广州华软 无名 一. 前言 在SharePoint中,不支持直接操作数据库,但开发过程中,避免不了查询数据,那么,在SharePoint中如何查询数据? 当然是使用CAML语法. 二. 目录 ...

  5. ArcGIS对SLD样式的支持

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.ArcGISWMS服务对SLD的支持 在完成用geoserver的w ...

  6. 为View设置左右切换动画

    本文同步自http://javaexception.com/archives/64 问题: 近期的需求中,碰到了一个view切换动画的需求.要实现的是点击按钮,从左到右滑动view,左边的view消失 ...

  7. 物理dataguard 正常切换 脚色转换,switchover_status 状态改变

    正常切换切换前: 主库:SQL> select DATABASE_ROLE from v$database;DATABASE_ROLE----------------PRIMARY SQL> ...

  8. ORACLE字符集修改ORA-02374\ORA-12899\ORA-02372

    IMPDP时部分日志显示这个警告ORA-02374: conversion error loading table "MEMXXX"."T_MEMBER_XXXX&quo ...

  9. IconFont的iOS使用

    IconFont的使用 Iconfont-国内功能很强大且图标内容很丰富的矢量图标库,提供矢量图标下载.在线存储.格式转换等功能.阿里巴巴体验团队倾力打造,设计和前端开发的便捷工具. https:// ...

  10. 在linux系统部署Svn

      步骤一:安装subversion 1) 连接xshell,在xshell输入命令:yum install subversion 2) 查看安装svn服务的版本:svnserve --version ...