前言

假设现在有个需求:监听滑动事件,并执行回调。
当你用触摸板或者鼠标滑动页面时,每秒钟大概会触发几十次scroll事件,而当你在手机
等移动终端上滑动页面时,每秒就会触发一百次scroll事件。如果我们的回调函数较为复杂,页面的性能就会变差。

解决问题的两种工具:debounce、throttle,它们有些类似,比如作用都是控制目标函数在一段时间内执行的次数;但更多的是不同:debounce使得在前后两次事件间隔不超过一定时间的情况下,无论触发多少次事件都只会执行一次回调函数。而throttle可以保证稳定的时间间隔执行一次回调函数。但需要弄清楚的是,无论是debounce还是throttle,控制的都是回调函数的执行,而不是事件的监听

另外,debounce和throttle都只是一种思想,可以有很多种实现,当然也可以自己去实现,后文中的代码都是基于lodash中的debounce和throttle。

debounce

想象这样一个场景:电梯即将关门,这时有个人上电梯,电梯就会停止关门。过了一会儿(间隔在电梯完全关上门所需要的时间之内),电梯又准备关门,又有人上电梯,又重复之前的步骤,直到最后一个人进来,电梯完全关上门,整个过程中电梯只关了一次门。

这个场景可以说是debounce在现实生活中的一个模型。回到代码层面:

// debounce(callback, millisecond, options)
var onScroll = debounce(animation, 1000, { leading: true, trailing: false });

// right
$('#container').addEventListener('scroll', onScroll);

// wrong
$('#container').addEventListener('scroll', function(){
    debounce(callback, millisecond, options);
});

debounce接受三个参数:要控制的函数、两次事件间隔的最大毫秒数、以及配置对象,返回一个函数,通常直接作为事件处理函数。详细说说第三个参数options,此参数默认值为:

{ leading: false,  trailing: true }

leading: 事件一被触发,先执行一次回调函数,再对之后的调用做控制。这样做的好处是事件一被触发,回调就执行,更真实;
trailing: 先对回调函数做控制,直到事件触发间隔超过设定时间,再调用回调函数,像上面电梯关门的例子
两者同时为true时,一次控制过程中回调会被执行两次;两者同时为false时,回调不执行。

常见的应用场景:拖拽窗口的大小、实时验证input

throttle

相比于debounce,throttle更像是一个特殊化的setInterval,就是说throttle包装过的函数会按固定的时间间隔执行,区别在于这个执行跟事件的触发有关,并且不用像setInterval那样手动取消。

throttle(callback, millisecond)

所以throttle更适用于需要不断执行但又需要控制执行次数来优化性能的函数,比如在滑动时根据滑动的数据(scrollTop等)不断改变某元素的样式。这种情况下,间隔时间设的过长就会不流畅,过短又起不到优化的效果。一般设为16ms,这样可以让帧率达到60fps,保持良好的视觉效果。说到这里就不得不提浏览器原生API requestAnimationFrame了。

粗略地说,requestAnimationFrame(callback)相当于

throttle(callback, 16);

rAF的优点在于它是原生的API,较为稳定。当然也有不少缺点,比如需要手动的启动和取消;浏览器tab不是active的时候不会被执行;不支持IE9;

caveat

  • 虽然说debounce、throttle有很多实现,甚至可以自己实现,但还是推荐直接使用loadash或者underscore,专业的工具库考虑到的事情往往比我们自己更多,不用担心为了使用两个函数而把整个lodash库都引入的问题,lodash包是可定制的,具体的方法自行Google。
  • 尽量将debounce或者throttle生成的函数直接作为事件处理函数,避免写出这种错误的代码:

    $('#container').addEventListener('scroll', function(){
        // 这里只是生成了函数,并没有执行,即使执行也无法达到控制的效果
       debounce(callback, millisecond, options);
    });
    
  • 使用变量保存debounce或者throttle返回的值后,可以调用取消的方法,就像setTimeout那样:

    onScroll = debounce(animation, 1000, { leading: true,             
    trailing: false });
    
    $('#container').addEventListener('scroll', onScroll);
    
    onScroll.cancel();
    

    参考文章: Debouncing and Throttling Explained Through Examples


simon_z
254 声望12 粉丝