ZooMze's World

vuePress-theme-reco ZooMze    2018 - 2021
ZooMze's World ZooMze's World

Choose mode

  • dark
  • auto
  • light
主页
分类
  • 基础
  • 备忘
  • 教程
  • 扩展
  • 框架
  • 组件
  • 季度分享
标签
时光轴
GitHub
author-avatar

ZooMze

35

Article

23

Tag

主页
分类
  • 基础
  • 备忘
  • 教程
  • 扩展
  • 框架
  • 组件
  • 季度分享
标签
时光轴
GitHub
  • 防抖 Debounce

    • 写在前面
      • 为啥要做防抖
        • 防抖 debounce
          • 初步实现
            • this 的指向错误
              • event去哪儿了
                • 我不想等
                  • 函数返回值
                    • 取消防抖

                    防抖 Debounce

                    vuePress-theme-reco ZooMze    2018 - 2021

                    防抖 Debounce


                    ZooMze 2020-07-07 JavaScript

                    # 写在前面

                    虽然前人栽的大树(lodash ) , (underscore )已经足够乘凉了, 但是用归用, 这防抖的知识以及原理还是要掌握呀, 那么本文就来探讨下这个, 参考了掘金上一堆文章, 然后自己来总结下。

                    还有一篇 节流 throttle 哦

                    # 为啥要做防抖

                    你是否遇到过写 mousemove 、scroll 或是 resize, 这类事件从触发开始到结束, 会以毫秒级不停调用非常非常多次, 这无疑加大了对内存的占用, 于是你的网页开始卡了...

                    就拿 mousemove来举例:

                    var container = document.getElementsByTagName('body')[0];
                    var count = 1
                    function getUserAction() {
                      console.log(`这是第${count++}次调用这个事件了!`);
                    };
                    
                    container.onmousemove = getUserAction;
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7

                    然后鼠标在页面上随便划拉几下, 就会发现事件疯狂调用了一百多次:

                    mousemove

                    当然, 现在浏览器处理这点小事还是绰绰有余, 即使毫秒级别为间隔也给你同步处理好了, 但是如果 getUserAction 函数内复杂一点, 例如大量的数据修改、 I/O、 异步请求等, 浏览器就不见得能顺滑地处理这么大批的任务了, 所以为了解决这个隐患, 就得请出本文的主角: 防抖 debounce

                    # 防抖 debounce

                    防抖的意思是: 如果你在一段时间内一直触发事件, 只允许 初始在第 n 秒后 或者 上一次事件执行完后的第 n 秒才开始再次执行, 如果在等待的这 n 秒内又触发了, 那么刷新这个计时器, 重新计算 0 ~ n 秒的这个过程。

                    简而言之就是: 等这个一直活跃的触发源头(onmousemove) 停下来之后的第 n 秒, 处理函数(getUserAction)才执行。

                    # 初步实现








                     
                     
                     
                     
                     
                     
                     

                     

                    var container = document.getElementsByTagName('body')[0];
                    var count = 1
                    
                    function getUserAction() {
                      console.log(`这是第${count++}次调用这个事件了!`);
                    };
                    
                    function debounce(func, wait) {
                      var timeout;
                      return function() {
                        clearTimeout(timeout) // 重新触发时重置定时器
                        timeout = setTimeout(func, wait);
                      }
                    }
                    
                    container.onmousemove = debounce(getUserAction, 1000);
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    14
                    15
                    16

                    现在变成了, 从你开始晃鼠标开始, 如果不停下来, getUserAction 这个函数是不会执行的, 极大的减少了不需要的内存占用!

                    # this 的指向错误

                    现在将打印的内容改为 console.log(this), 看看在使用和不使用刚刚写的防抖函数的情况下, 输出了些什么

                    mousemove mousemove

                    正确的指向应该是 body 对象, 但由于

                    关于这个 this 指向问题, 先看如下这个小例子, 或者看 this 的指向规则:

                    var num = 1
                    var obj = {
                      num: 2,
                      getNum: function name1() {
                        console.log(this) // 指向obj
                        return (function () {
                          console.log(this) // 指向window
                          return this.num;
                        })();
                      }
                    }
                    
                    obj.getNum(); // >> 1
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13

                    言归正传, 请出我们的借刀杀..哦不借物达人 call / apply 来将this指向变回 body 对象:





                     



                     














                    var container = document.getElementsByTagName('body')[0];
                    var count = 1
                    
                    function debounce(func, wait) {
                      var timeout;
                      console.log(this) // >> window
                      return function() {
                        var self = this; // 这里的 this 实际指向 <body>
                    
                        clearTimeout(timeout)
                        timeout = setTimeout(function() {
                          func.apply(self) // 用 call() 也一样
                        }, wait);
                      }
                    }
                    
                    function getUserAction() {
                      console.log(this); // >> <body>
                      console.log(`这是第${count++}次调用这个事件了!`);
                    };
                    
                    container.onmousemove = debounce(getUserAction, 1000);
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    14
                    15
                    16
                    17
                    18
                    19
                    20
                    21
                    22

                    好, 相信你已经看过关于 this 的内容了, 解释一下这里为啥 this 有不同的指向:

                    • 首先 debounce 是在 window 环境下执行的, 所以在 debounce 内部, this 是指向 window
                    • 然后 debounce 返回了一个函数, 这个函数真正被 onmousemove 执行, 所以在这里的 this 指向 <body>

                    好了, 现在指向正确啦!

                    # event去哪儿了

                    处理完了 this , 我们再来检查一下加入防抖后的JavaScript事件自带的 event 对象是否正确:

                    function getUserAction(e) {
                      console.log(e);
                    };
                    
                    1
                    2
                    3

                    它果然不负众望地整错了→_→ , 它输出了 undefined , 这是因为在借方法时没有把默认参数带上, 那么我们把该有的参数还给它, 修改 debounce 函数






                     



                     




                    function debounce(func, wait) {
                      var timeout;
                    
                      return function() {
                        var self = this;
                        var args = arguments; // 引用函数的实参
                    
                        clearTimeout(timeout)
                        timeout = setTimeout(function() {
                          func.apply(self, args) // 这里就不能用call了
                        }, wait);
                      }
                    }
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13

                    # 我不想等

                    到目前为止, 还有个小问题, 那就是防抖函数必须要 '等' 到计时结束, 才会执行第一次, 如果间隔较短, 那用户还能忍, 但是防抖间隔较长, 就没那么好受了, 于是我们再来改造一下它, 让它可以立即执行并在停止 n 秒后才可以重新触发:









                     
                     
                     
                     
                     
                     
                     
                     








                    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时长结束后清空计时器, 防止多次进入执行
                          }, wait)
                          if (callNow) func.apply(context, args)
                        }
                        else {
                          timeout = setTimeout(function() {
                            func.apply(context, args)
                          }, wait);
                        }
                      }
                    }
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    14
                    15
                    16
                    17
                    18
                    19
                    20
                    21
                    22
                    23

                    # 函数返回值

                    一个函数除了有参数 , 还会有返回值哦, 所以 getUserAction() 也是可能有返回值的哦, 在上一版的代码中给 getUserAction() 添加返回值, 再调用这个事件, 会发现return undefined

                    functionReturn

                    当 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时长结束后清空计时器, 防止多次进入执行
                          }, wait)
                          if (callNow) result = func.apply(context, args)
                        }
                        else {
                          timeout = setTimeout(function() {
                            func.apply(context, args)
                          }, wait);
                        }
                        return result
                      }
                    }
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    14
                    15
                    16
                    17
                    18
                    19
                    20
                    21
                    22
                    23
                    24

                    functionReturn

                    # 取消防抖

                    最后一个问题了, 如果我想取消防抖该咋整呢 , 这也是一个很合理的需求, 那么我们开始吧 , 首先让 debounce 函数 return 一个变量 debonced, 并且给这个变量注册一个 cancel 方法。





                     


















                     
                     
                     
                     



                    function debounce(func, wait, immediate) {
                    
                      var timeout, result;
                    
                      var debonced = function() {
                        var context = this;
                        var args = arguments;
                    
                        if (timeout) clearTimeout(timeout);
                        if (immediate) { // 需要立即执行
                          var callNow = !timeout; // 如果已有计时器 , 则不会执行
                          timeout = setTimeout(function() {
                            timeout = null; // wait时长结束后清空计时器, 防止多次进入执行
                          }, 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
                    }
                    
                    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

                    我们来试用一下这个取消防抖

                    ...
                    var setUseAction = debounce(getUserAction, 10000, true);
                    container.onmousemove = setUseAction;
                    
                    // 随便找个元素
                    document.getElementById("button").addEventListener('click', function() {
                      setUseAction.cancel();
                    })
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8

                    至此, 防抖函数完成