美文网首页
JavaScript-函数防抖

JavaScript-函数防抖

作者: 前端极客技术 | 来源:发表于2020-06-23 10:05 被阅读0次

    前言

    在前端开发过程中,我们会遇到一些频繁触发的事件,但我们需要控制回调的频率,比如下面几种场景:

    • 游戏中的按键响应,比如格斗,比如射击,需要控制出拳和射击的速率。
    • 自动完成,按照一定频率分析输入,提示自动完成。
    • 鼠标移动和窗口滚动,鼠标稍微移动一下,窗口稍微滚动一下会带来大量的事件,因而需要控制回调的发生频率。

    下面我们通过代码来看看mousemove事件是如何频繁触发的:

    index.html文件代码如下:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8">
            <title>debounce防抖</title>
            <style>
              #container{
                width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
              }
            </style>
        </head>
        <body>
            <div id="container"></div>
            
        <script src="debounce.js"></script>
            <script>
                var count = 1;
                var container = document.getElementById("container");
                
                function mouseMove() {
                    console.log(this);
                    container.innerHTML = count++;
                };
                
                container.onmousemove = mouseMove
            </script>
        </body>
    </html>
    

    运行该html文件,我们将鼠标在我们定义的矩形区域移动,只是简单的从下往上滑动,mouseMove函数就被触发了99次。

    java-debounce-1.gif

    假设mouseMove函数时复杂的回调函数或者是ajax请求,如果我们没有对事件处理函数调用的频率进行限制,会加重浏览器的负担,导致用户体验极差。这时候我们可以采用debounce(防抖)或throttle(节流)的方式来减少调用频率,同时又不影响实际效果。

    今天我们主要讲讲防抖。

    欢迎关注我的微信公众号:前端极客技术(FrontGeek)

    防抖

    原理

    函数防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

    初步实现

    根据上面防抖的描述,我们可以用setTimeout写第一版防抖函数的实现代码:

    function debounce(func, wait) {
      var timeout
      return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait)
      }
    }
    

    在最开始的例子中使用debounce:

    container.onmousemove = debounce(mouseMove, 1000)
    

    这样子修改后,只有在我们移动完1s内不再触发,才会执行回调事件mouseMove。

    this指向问题

    我们在mouseMove函数中执行 console.log(this),会发现不使用debounce和使用debounce情况下,this的值是不一样的。

    不使用debounce时this的值为

    <div id="container"></div>
    

    而使用debounce函数时,this则会指向window对象。

    所以我们需要将this指向正确的函数,这时候我们可以利用apply()方法实现。代码修改如下:

    function debounce(func, wait) {
      var timeout
      return function () {
        var context = this
        clearTimeout(timeout)
        timeout = setTimeout(function() {
          func.apply(context)
        }, wait)
      }
    }
    

    修改完成后,我们再触发事件,可以看到此时this的指向正确了。

    event对象

    JavaScript在事件处理函数中会提供事件对象event,我们将mouseMove函数修改如下:

    function mouseMove(e) {
        console.log(this);
        console.log(e);
        container.innerHTML = count++;
    };
    

    如果不使用debounce函数,控制台打印的e是MouseEvent对象,但当我们调用debounce函数,打印出来的却是undefined

    所以我们再次修改代码如下:

    function debounce(func, wait) {
      var timeout
      return function () {
        var context = this
        var args = arguments
    
        clearTimeout(timeout)
        timeout = setTimeout(function() {
          func.apply(context, args)
        }, wait)
      }
    }
    

    至此,我们修复了this指向和event对象问题,整个防抖函数已经算是比较完善了。

    立即执行

    接下来我们再考虑一个新的需求:如果我们希望是在事件一触发就立刻执行函数,而不是等到事件停止触发后再执行;并且等到停止触发n秒后,才可以重新触发执行。

    我们可以通过immediate参数来判断是否立刻执行,代码修改如下:

    function debounce(func, wait, immediate) {
      var timeout
      return function () {
        var context = this
        var args = arguments
    
        if (timeout) {
                clearTimeout(timeout)
            }
            if (immediate) {
                // 已经执行过不再执行
                var callNow = !timeout
                timeout = setTimeout(function() {
                    timeout = null
                }, wait)
                if (callNow) {
                    func.apply(context, args)
                }
            } else {
                timeout = setTimeout(function() {
                  func.apply(context, args)
                }, wait)
            }
      }
    }
    
    container.onmousemove = debounce(mouseMove, 1000, true)
    

    返回值

    我们需要注意的一点是:mouseMove函数可能是有返回值的,所以我们也要返回函数的执行结果,但是immediate为false的时候,因为使用setTimeout,我们将func.apply(context, args)的返回值赋给变量,最后再return的时候,值会一直是undefined,所以我们只在immediate为true的时候返回函数的执行结果。

    function debounce(func, wait, immediate) {
      var timeout, result
      return function () {
        var context = this
        var args = arguments
    
        if (timeout) {
                clearTimeout(timeout)
            }
            if (immediate) {
                // 已经执行过不再执行
                var callNow = !timeout
                timeout = setTimeout(function() {
                    timeout = null
                }, wait)
                if (callNow) {
                    result = func.apply(context, args)
                }
            } else {
                timeout = setTimeout(function() {
                  func.apply(context, args)
                }, wait)
            }
            return result
      }
    }
    

    取消

    假设防抖的时间间隔为10秒,immediate为true的情况下,只有等10秒后才能重新触发事件,这时候我希望有个按钮可以取消防抖,这样我再去触发时,又可以立即执行了。

    下面我们来实现这个取消功能:

    function debounce(func, wait, immediate) {
      var timeout, result
      var debounced = function () {
        var context = this
        var args = arguments
    
        // 每次新的尝试调用func,会使抛弃之前等待的func
        if (timeout) clearTimeout(timeout)
    
        // 如果允许新的调用尝试立即执行
            if (immediate) {
                // 如果之前尚没有调用尝试,那么此次调用可以立马执行,否则就需要等待
                var callNow = !timeout
          // 刷新timeout
                timeout = setTimeout(function() {
                    timeout = null
                }, wait)
          // 如果能被立即执行,立即执行
                if (callNow) result = func.apply(context, args)
            } else {
                timeout = setTimeout(function() {
                  func.apply(context, args)
                }, wait)
            }
            return result
      }
        
        debounced.cancel = function () {
            clearTimeout(timeout)
            timeout = null
        }
        return debounced
    }
    

    如何调用这个cancel函数?

    var setMouseMove = debounce(mouseMove, 10000, true)
    container.onmousemove = setMouseMove
    
    // buttonClick为button的click事件
    function buttonClick() {
      setMouseMove.cancel()
    }
    

    效果如下:


    java-debounce-2.gif

    到这里,一个完整的debounce函数已经实现了。

    总结

    debounce防抖函数,满足的是:高频下只响应一次。

    在实际开发过程中,常见的应用场景有:

    • 在输入框快速输入文字(高频),我们只想在其完全停止输入时再对输入文字做处理(一次)
    • ajax,大多数场景下,每个异步请求在短时间内只能响应一次,比如下拉刷新、不停地上拉加载,但只发送一次ajax请求。

    欢迎关注微信公众号:前端极客技术

    相关文章

      网友评论

          本文标题:JavaScript-函数防抖

          本文链接:https://www.haomeiwen.com/subject/nojcfktx.html