• Debounce 和 Throttle 的原理及实现---防止频繁触发某事件


    原文:http://blog.csdn.net/redtopic/article/details/69396722

    在处理诸如 resizescrollmousemove 和 keydown/keyup/keypress 等事件的时候,通常我们不希望这些事件太过频繁地触发,尤其是监听程序中涉及到大量的计算或者有非常耗费资源的操作。

    有多频繁呢?以 mousemove 为例,根据 DOM Level 3 的规定,「如果鼠标连续移动,那么浏览器就应该触发多个连续的 mousemove 事件」,这意味着浏览器会在其内部计时器允许的情况下,根据用户移动鼠标的速度来触发 mousemove 事件。(当然了,如果移动鼠标的速度足够快,比如“刷”一下扫过去,浏览器是不会触发这个事件的)。resizescroll 和 key* 等事件与此类似。

    可以参看这个 Demo 体会下。

    Debounce

    DOM 事件里的 debounce 概念其实是从机械开关和继电器的“去弹跳”(debounce)衍生 出来的,基本思路就是把多个信号合并为一个信号。这篇文章 解释得非常清楚,感兴趣的可以一读。

    在 JavaScript 中,debounce 函数所做的事情就是,强制一个函数在某个连续时间段内只执行一次,哪怕它本来会被调用多次。我们希望在用户停止某个操作一段时间之后才执行相应的监听函数,而不是在用户操作的过程当中,浏览器触发多少次事件,就执行多少次监听函数。

    比如,在某个 3s 的时间段内连续地移动了鼠标,浏览器可能会触发几十(甚至几百)个 mousemove 事件,不使用 debounce 的话,监听函数就要执行这么多次;如果对监听函数使用 100ms 的“去弹跳”,那么浏览器只会执行一次这个监听函数,而且是在第 3.1s 的时候执行的。

    现在,我们就来实现一个 debounce 函数。

    实现

    我们这个 debounce 函数接收两个参数,第一个是要“去弹跳”的回调函数 fn,第二个是延迟的时间 delay

    实际上,大部分的完整 debounce 实现还有第三个参数 immediate ,表明回调函数是在一个时间区间的最开始执行(immediate 为 true)还是最后执行(immediate 为 false),比如 underscore 的 _.debounce。本文不考虑这个参数,只考虑最后执行的情况,感兴趣的可以自行研究。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    /**
    *
    * @param fn {Function}   实际要执行的函数
    * @param delay {Number}  延迟时间,也就是阈值,单位是毫秒(ms)
    *
    * @return {Function}     返回一个“去弹跳”了的函数
    */
    function debounce(fn, delay) {
    
      // 定时器,用来 setTimeout
      var timer
    
      // 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数
      return function () {
    
        // 保存函数调用时的上下文和参数,传递给 fn
        var context = this
        var args = arguments
    
        // 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
        clearTimeout(timer)
    
        // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
        // 再过 delay 毫秒就执行 fn
        timer = setTimeout(function () {
          fn.apply(context, args)
        }, delay)
      }
    }
    

    其实思路很简单,debounce 返回了一个闭包,这个闭包依然会被连续频繁地调用,但是在闭包内部,却限制了原始函数 fn 的执行,强制 fn 只在连续操作停止后只执行一次。

    debounce 的使用方式如下:

    1
    2
    3
    
    $(document).on('mouvemove', debounce(function(e) {
    	// 代码
    }, 250))
    

    用例

    还是以 mousemove 为例,为其绑定一个“去弹跳”的监听器,效果是怎样的?请看这个 Demo

    再来考虑另外一个场景:根据用户的输入实时向服务器发 ajax 请求获取数据。我们知道,浏览器触发 key* 事件也是非常快的,即便是正常人的正常打字速度,key* 事件被触发的频率也是很高的。以这种频率发送请求,一是我们并没有拿到用户的完整输入发送给服务器,二是这种频繁的无用请求实在没有必要。

    更合理的处理方式是,在用户“停止”输入一小段时间以后,再发送请求。那么 debounce 就派上用场了:

    1
    2
    3
    
    $('input').on('keyup', debounce(function(e) {
    	// 发送 ajax 请求
    }, 300))
    

    可以查看这个 Demo 看看效果。

    Throttle

    throttle 的概念理解起来更容易,就是固定函数执行的速率,即所谓的“节流”。正常情况下,mousemove 的监听函数可能会每 20ms(假设)执行一次,如果设置 200ms 的“节流”,那么它就会每 200ms 执行一次。比如在 1s 的时间段内,正常的监听函数可能会执行 50(1000/20) 次,“节流” 200ms 后则会执行 5(1000/200) 次。

    我们先来看 Demo。可以看到,不管鼠标移动的速度是慢是快,“节流”后的监听函数都会“匀速”地每 250ms 执行一次。

    实现

    与 debounce 类似,我们这个 throttle 也接收两个参数,一个实际要执行的函数 fn,一个执行间隔阈值 threshhold

    同样的,throttle 的更完整实现可以参看 underscore 的 _.throttle

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    /**
    *
    * @param fn {Function}   实际要执行的函数
    * @param delay {Number}  执行间隔,单位是毫秒(ms)
    *
    * @return {Function}     返回一个“节流”函数
    */
    
    function throttle(fn, threshhold) {
    
      // 记录上次执行的时间
      var last
    
      // 定时器
      var timer
    
      // 默认间隔为 250ms
      threshhold || (threshhold = 250)
    
      // 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数
      return function () {
    
        // 保存函数调用时的上下文和参数,传递给 fn
        var context = this
        var args = arguments
    
        var now = +new Date()
    
        // 如果距离上次执行 fn 函数的时间小于 threshhold,那么就放弃
        // 执行 fn,并重新计时
        if (last && now < last + threshhold) {
          clearTimeout(timer)
    
          // 保证在当前时间区间结束后,再执行一次 fn
          timer = setTimeout(function () {
            last = now
            fn.apply(context, args)
          }, threshhold)
    
        // 在时间区间的最开始和到达指定间隔的时候执行一次 fn
        } else {
          last = now
          fn.apply(context, args)
        }
      }
    }
    

    原理也不复杂,相比 debounce,无非是多了一个时间间隔的判断,其他的逻辑基本一致。throttle 的使用方式如下:

    1
    2
    3
    
    $(document).on('mouvemove', throttle(function(e) {
    	// 代码
    }, 250))
    

    用例

    throttle 常用的场景是限制 resize 和 scroll 的触发频率。以 scroll 为例,查看这个 Demo 感受下。

    可视化解释

    如果还是不能完全体会 debounce 和 throttle 的差异,可以到 这个页面 看一下两者可视化的比较。

    图片

    总结

    debounce 强制函数在某段时间内只执行一次,throttle 强制函数以固定的速率执行。在处理一些高频率触发的 DOM 事件的时候,它们都能极大提高用户体验。

  • 相关阅读:
    1363:小球(drop)
    ifstream ofstream c++中读写文件
    线性筛
    Network of Schools POJ
    Beautiful numbers CodeForces
    ipone6界面设计标准
    目前的前端框架有哪些
    什么是react native
    nth-child() 选择器
    html{height:100%}的意义以及body背景色的解析推断
  • 原文地址:https://www.cnblogs.com/zhangruiqi/p/8404860.html
Copyright © 2020-2023  润新知