(转)JavaScript-性能优化之函数节流(throttle)与函数去抖(debounce)

摘要:
简单来说,JavaScript性能优化的函数节流和函数去抖动功能节流就是使函数在极短的时间间隔内无法连续调用。下一个函数调用只能在上次函数执行超过指定的时间间隔后进行。对于这两个需求,有两种解决方案:去抖动和节流。Throwle和debouck是解决请求和响应速度不匹配问题的两种解决方案。抛出以相等的间隔执行函数。如果事件在反跳时间间隔t内再次触发,则将再次计时,直到停止时间大于或等于t。
 JavaScript-性能优化之函数节流(throttle)与函数去抖(debounce)
        函数节流,简单地讲,就是让一个函数无法在很短的时间间隔内连续调用,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。
        函数节流的原理挺简单的,估计大家都想到了,那就是定时器。当我触发一个时间时,先setTimout让这个事件延迟一会再执行,如果在这个时间间隔内又触发了事件,那我们就clear掉原来的定时器,再setTimeout一个新的定时器延迟一会执行,就这样。
以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。
  1. window对象的resize、scroll事件
  2. 拖拽时的mousemove事件
  3. 射击游戏中的mousedown、keydown事件
  4. 文字输入、自动完成的keyup事件
          实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce和throttle两种解决办法。
throttle 和 debounce 是解决请求和响应速度不匹配问题的两个方案。二者的差异在于选择不同的策略。
        throttle 等时间 间隔执行函数。
        debounce 时间间隔 t 内若再次触发事件,则重新计时,直到停止时间大于或等于 t 才执行函数。
一、throttle函数的简单实现
function throttle(fn, threshhold, scope) {
    threshhold || (threshhold = 250);
    var last,
        timer;
    return function () {
        var context = scope || this;
        var now = +new Date(),
            args = arguments;
        if (last && now - last + threshhold < 0) {
            // hold on to it
            clearTimeout(deferTimer);
            timer = setTimeout(function () {
                last = now;
                fn.apply(context, args);
            }, threshhold);
        } else {
            last = now;
            fn.apply(context, args);
        }
    };
}

调用方法

$('body').on('mousemove', throttle(function (event) {
  console.log('tick');
}, 1000));
二、debounce函数的简单实现
function debounce(fn, delay) {
    var timer = null;
    return function () {
        var context = this, args = arguments;
        clearTimeout(timer);
        timer = setTimeout(function () {
            fn.apply(context, args);
        }, delay);
    };
}

调用方法

$('input.username').keypress(debounce(function (event) {
  // do the Ajax request
}, 250));
三、简单的封装实现
/**
  * throttle 
  * @param fn, wait, debounce
  */
var throttle = function ( fn, wait, debounce ) {
    var timer = null, // 定时器
        t_last = null, // 上次设置的时间
        context, // 上下文
        args, // 参数
        diff; // 时间差
    return funciton () {
        var curr = + new Date();
        var context = this, args = arguments;
        clearTimeout( timer );
        if ( debounce ) { // 如果是debounce
            timer = setTimeout( function () {
                fn.apply( context, args );
            }, wait );
        } else { // 如果是throttle
            if ( !t_last ) t_last = curr;
            if ( curr - t_last &gt;= wait ) {
                fn.apply( context, wait );
                context = wait = null;
            }
        }
    }
}
/**
  * debounce
  * @param fn, wait
  */
var debounce = function ( fn, wait ) {
    return throttle( fn, wait, true );
}
小结:这两个方法适用于会重复触发的一些事件,如:mousemove,keydown,keyup,keypress,scroll等。如果只绑定原生事件,不加以控制,会使得浏览器卡顿,用户体验差。为了提高js性能,建议在使用以上及类似事件的时候用函数节流或者函数去抖加以控制。
四、underscore v1.7.0相关的源码剖析                          
1. _.throttle函数
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;     // 定时器
    var previous = 0;       // 上次触发的时间
    if (!options) options = {};
    var later = function() {
        previous = options.leading === false ? 0 : _.now();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };
    return function() {
        var now = _.now();

        // 第一次是否执行
        if (!previous &amp;&amp; options.leading === false) previous = now;

        // 这里引入了一个remaining的概念:还剩多长时间执行事件
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        // remaining &lt;= 0 考虑到事件停止后重新触发或者
        // 正好相差wait的时候,这些情况下,会立即触发事件
        // remaining &gt; wait 没有考虑到相应场景
        // 因为now-previous永远都是正值,且不为0,那么
        // remaining就会一直比wait小,没有大于wait的情况
        // 估计是保险起见吧,这种情况也是立即执行
        if (remaining &lt;= 0 || remaining &gt; wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;

            // 是否跟踪
        } else if (!timeout &amp;&amp; options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };
};
由上可见,underscore考虑了比较多的情况:
options.leading: 第一次是否执行,默认为true,表示第一次会执行,传入{leading:false}则禁用第一次执行
options.trailing:最后一次是否执行,默认为true,表示最后一次会执行,传入{trailing: false}表示最后一次不执行
所谓第一次是否执行,是刚开始触发事件时,要不要先触发事件,如果要,则previous=0,remaining 为负值,则立即调用了函数
所谓最后一次是否执行,是事件结束后,最后一次触发了此方法,如果要执行,则设置定时器,即事件结束以后还要在执行一次。
remianing > wait 表示客户端时间被修改过。
2. _.debounce函数 
_.debounce = function(func, wait, immediate) {
    // immediate默认为false
    var timeout, args, context, timestamp, result;
    var later = function() {
        // 当wait指定的时间间隔期间多次调用_.debounce返回的函数,则会不断更新timestamp的值,导致last < wait && last >= 0一直为true,从而不断启动新的计时器延时执行func
        var last = _.now() - timestamp;
        if (last < wait && last >= 0) {
            timeout = setTimeout(later, wait - last);
        } else {
            timeout = null;
            if (!immediate) {
                result = func.apply(context, args);
                if (!timeout) context = args = null;
            }
        }
    };
    return function() {
        context = this;
        args = arguments;
        timestamp = _.now();
        // 第一次调用该方法时,且immediate为true,则调用func函数
        var callNow = immediate && !timeout;
        // 在wait指定的时间间隔内首次调用该方法,则启动计时器定时调用func函数
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
            context = args = null;
        }
        return result;
    };
};  
 _.debounce实现的精彩之处我认为是通过递归启动计时器来代替通过调用clearTimeout来调整调用func函数的延时执行。
参考链接:
http://www.cnblogs.com/fsjohnhuang/p/4147810.html
http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout-said-the-event-loop-model/#prettyPhoto

免责声明:文章转载自《(转)JavaScript-性能优化之函数节流(throttle)与函数去抖(debounce)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇001_Three.js中的跨域问题mac格式化重装系统下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Linux Socket学习(十二)

套接口选项在前面的几章中,我们讨论了使用套接口的基础内容。现在我们要来探讨一些可用的其他的特征。在我们掌握了这一章的概念之后,我们就为后面的套接口的高级主题做好了准备。在这一章,我们将会专注于下列主题:如何使用getsockopt(2)函数获得套接口选项值如何使用setsockopt(2)函数设置套接口选项值如何使用这些常用的套接口选项得到套接口选项有时,...

关于DOM的操作以及性能优化问题-重绘重排

写在前面: 大家都知道DOM的操作很昂贵。  然后贵在什么地方呢? 一、访问DOM元素 二、修改DOM引起的重绘重排 一、访问DOM   像书上的比喻:把DOM和JavaScript(这里指ECMScript)各自想象为一个岛屿,它们之间用收费桥梁连接,ECMAScript每次访问DOM,都要途径这座桥,并交纳“过桥费”,访问DOM的次数越多,费用也就越高...

提高代码质量的三要素

程序员在职业生涯中难免要接受编程面试。有些程序员由于平时没有养成良好的编程习惯,在面试时写出的代码质量不高,最终遗憾地与心仪的公司和职位失之交臂。因此,如何在面试时能写出高质量的代码,是很多程序员关心的问题。         代码的规范性        面试官是根据应聘者写出的代码来决定是否录用一个应聘者的。应聘者首先要把代码写得规范,才可以避免很多低...

0x01 译文:Windows桌面应用Win32开发简介

本节课将简单介绍下使用C++开发Windows桌面应用的一些基础知识  目录: 准备你的开发环境 Windows 代码规范 操作字符串 什么是一个Window? WinMain:程序的入口点 1. 准备你的开发环境 安装 Windows SDK        要用C或者C++开发Windows 程序,你必须安装 Microsoft Windows So...

C++ 常见崩溃问题分析

一、前言 从事自动化测试平台开发的编程实践中,遭遇了几个程序崩溃问题,解决它们颇费了不少心思,解决过程中的曲折和彻夜的辗转反侧却历历在目,一直寻思写点东西,为这段难忘的经历留点纪念,总结惨痛的教训带来的经验,以期通过自己的经历为他人和自己带来福祉:写出更高质量的程序; 由于 C 和 C++ 这两种语言血缘非常近,文本亦对 C 编程语言有借鉴作用; 二、C+...

Java 性能优化的 50 个细节

在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。 #尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例 简单来说,单例主要适用于以下三个方面: 控制资源的使用,通过线程同步来控制资源的并发访问; 控制实例的产生,以...