Debounce vs Throttle

news/2023/12/9 17:04:16

我们在处理事件的时候,有些事件由于触发太频繁,而每次事件都处理的话,会消耗太多资源,导致浏览器崩溃。最常见的是我们在移动端实现无限加载的时候,移动端本来滚动就不是很灵敏,如果每次滚动都处理的话,界面就直接卡死了。

因此,我们通常会选择,不立即处理事件,而是在触发一定次数或一定时间之后进行处理。这时候我们有两个选择: debounce(防抖动)和 throttle(节流阀)。

之前看过很多文章都还是没有太弄明白两者之间的区别,最后通过看源码大致了解了两者之间的区别以及简单的实现思路。

首先,我们通过实践来最简单的看看二者的区别:

图片描述

可以看到,throttle会在第一次事件触发的时候就执行,然后每隔wait(我这里设置的2000ms)执行一次,而debounce只会在事件结束之后执行一次。

有了一个大概的印象之后,我们看一看lodash的源码对debouncethrottle的区别。

这里讨论默认情况

function throttle(func, wait, options) {
  let leading = true,
    trailing = true;

  if (typeof func !== 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (typeof options === 'object') {
    leading = 'leading' in options
      ? !!options.leading
      : leading;
    trailing = 'trailing' in options
      ? !!options.trailing
      : trailing;
  }
  return debounce(func, wait, {
    leading,
    maxWait: wait,
    trailing,
  });
}

可以看到,throttle最后返回的还是debounce函数,只是指定了options选项。那么接下来我们就集中分析debounce

function debounce(fn, wait, options) {
    var lastArgs,
      lastThis,
          maxWait,
          result,
          timerId,
          lastCallTime,
          lastInvokeTime = 0,
          leading = false,
          maxing = false,
          trailing = true;
      function debounced() {
        var time = now(),
            isInvoking = shouldInvoke(time);

        lastArgs = arguments;
        lastThis = this;
        lastCallTime = time;

        if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }
        return result;
      }
      debounced.cancel = cancel;
      debounced.flush = flush;
      return debounced;
}

为了记录每次执行的相关信息,debounce函数最后返回的是一个函数,形成一个闭包。

这也解释了为什么这样写不行:

    window.addEventListener('resize', function(){
      _.debounce(onResize, 2000);
    });

这样写根本就不会调用内部的debounced函数。

解决第一个不同

debounced内部呢,首先记录了当前调用的时间,然后通过shouldInvoke这个函数判断是否应该调用传入的func

      function shouldInvoke(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime;

        // Either this is the first call, activity has stopped and we're at the
        // trailing edge, the system time has gone backwards and we're treating
        // it as the trailing edge, or we've hit the `maxWait` limit.
        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
      }

可以看到,该函数返回true的几个条件。其中需要我们引起注意的是最后一个条件,这是debouncethrottle的区别之一。

首先maxing通过函数开始的几行代码判断:

      if (isObject(options)) {
        leading = !!options.leading;
        maxing = 'maxWait' in options;
        maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
        trailing = 'trailing' in options ? !!options.trailing : trailing;
      }

我们看到,在定义throttle的时候, 给debounce函数给传入了options, 而里面包含maxWait这个属性,因此,对于throttle来说,maxingtrue, 而没有传入optionsdebounce则为false。这就是二者区别之一。在这里决定了shouldInvoke函数返回的值,以及是否执行接下去的逻辑判断。

我们再回到debounced这个函数:

  if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }

在第一次调用的时候,debouncethrottleisInvoking
true, 且此时timerId === undefined也成立,就返回leadingEdge(lastCallTime)这个函数。

那么我们再来看看leadingEdge 这个函数;

      function leadingEdge(time) {
        // Reset any `maxWait` timer.
        lastInvokeTime = time;
        // Start the timer for the trailing edge.
        timerId = setTimeout(timerExpired, wait);
        // Invoke the leading edge.
        return leading ? invokeFunc(time) : result;
      }

这里出现了debouncethrottle的第二个区别。这个函数首先是设置了一个定时器,随后返回的结果由leading决定。在默认情况下,throttle传入的leadingtrue,而debouncefalse。因此,throttle会马上执行传入的函数,而debounce不会。

这里我们就解决了它们的第一个不同:throttle会在第一次调用的时候就执行,而debounce不会。

解决第二个不同

我们再回到shouldInvoke的返回条件那里,如果在一个时间内频繁的调用, 前面三个条件都不会成立,对于debounce来说,最后一个也不会成立。而对于throttle来说,首先maxingtrue, 而如果距离上一次*传入的func 函数调用 大于maxWait最长等待时间的话,它也会返回true

function shouldInvoke(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime;

        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
      }
        if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }

那么在shouldInvoke成立之后,throttle会设置一个定时器,返回执行传入函数的结果。

这就是debouncethrottle 之间的第二个区别:throttle会保证你每隔一段时间都会执行,而debounce不会。

那么还有最后一个问题,那我之前设置的定时器怎么办呢?

timerId = setTimeout(timerExpired, wait);

定时器执行的是timerExpired这个函数,而这个函数又会通过shouldInvoke进行一次判断。

function timerExpired() {
        var time = now();
        if (shouldInvoke(time)) {
          return trailingEdge(time);
        }
        // Restart the timer.
        timerId = setTimeout(timerExpired, remainingWait(time));
      }

最后,传入的func怎么执行的呢?下面这个函数实现:

function invokeFunc(time) {
        var args = lastArgs,
            thisArg = lastThis;

        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        result = func.apply(thisArg, args);
        return result;
      }

饿了么的简单实现

在看饿了么的infinite scroll这个源码的时候,看到了一个简单版本的实现:

var throttle = function (fn, delay) {
  var now, lastExec, timer, context, args; 

  var execute = function () {
    fn.apply(context, args);
    lastExec = now;
  };

  return function () {
    context = this;
    args = arguments;

    now = Date.now();

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (lastExec) {
      var diff = delay - (now - lastExec);
      if (diff < 0) {
        execute();
      } else {
        timer = setTimeout(() => {
          execute();
        }, diff);
      }
    } else {
      execute();
    }
  };
};

那么它的思路很简单:

    1. 通过lastExec判断是否是第一次调用,如果是,就马上执行处理函数。

    1. 随后就会监测,每次调用的时间与上次执行函数的时间差,如果小于0,就立马执行。大于0就会在事件间隔之后执行。

    1. 每次调用的时候都会清除掉上一次的定时任务,这样就会保证只有一个最近的定时任务在等待执行。

那么它与lodash的一个最大的区别呢,就是它是关注与上次执行处理函数的时间差, 而lodashshouldInvoke关注的是两次事件调用函数的时间差

总结

总的来说,这种实现的主要部分呢,就是时间差定时器

最后,自己参照写了简单的debouncethrottle: Gist求指教!

参考资料

  • debouncing-throttling-explained-examples | CSS-Tricks

  • Lodash源码

  • 饿了么 vue-infinite-scroll


http://www.niftyadmin.cn/n/4524796.html

相关文章

ValueError: Data must be aligned to block boundary in ECB mode

指的是 &#xff1a; data 数据长度要是这个 block length 的倍数 解决方法&#xff1a; class EncryptData():def __init__(self,key):self.key keyself.lenth DES.block_size # 加密数据的长度self.dec DES.new(key,DES.MODE_ECB) # 创建DES实例def add_8(self,info…

AttributeError: ‘list‘ object has no attribute ‘strip‘、‘split‘

原因&#xff1a; strip()、split()方法作用对象是一个字符串&#xff0c;使用时要注意自己处理的或之前用方法生成的是列表&#xff0c;集合&#xff0c;还是其他的什么&#xff0c;只要最后能转变成字符串就能用split()、strip()方法 解决办法&#xff1a; 把列表变成字符…

EXCEL中的数据分析—描述统计

今天给大家分享的是在数据分析中很重要的一环&#xff0c;也就是描述统计。在百科的解释中&#xff0c;描述统计是通过图表或数学方法&#xff0c;对数据资料进行整理、分析&#xff0c;并对数据的分布状态、数字特征和随机变量之间关系进行估计和描述的方法。描述统计分为集中…

django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module. Did you install mysqlclie

环境&#xff1a; Django 3.2.6 原因 &#xff1a;没有安装mysqlclient 解决办法&#xff1a; pip install pymysql pip install mysqlclient 这个问题解决

集合set简单介绍

set集合是一个无序不重复元素的集&#xff0c;基本功能包括关系测试和消除重复元素。集合使用大括号({})框定元素&#xff0c;并以逗号进行分隔。但是注意&#xff1a;如果要创建一个空集合&#xff0c;必须用 set() 而不是 {} &#xff0c;因为后者创建的是一个空字典。集合除…

Django中分页器的使用

1.子应用中的view.py from paginator_app.models import Student from django.shortcuts import render from .models import Student # Create your views here. from django.core.paginator import Paginator,Page from django.conf import settings def student_list(reque…

python递归实现获取所有的字典键

之前看过一篇博客&#xff0c;对递归讲的比较好&#xff1a; 递归的调用是&#xff1a; 回溯&#xff1a;每次向深层次进行不断调用&#xff0c;称之为回溯 递推&#xff1a;回溯到某个层次&#xff0c;然后会停止&#xff0c;然后一层层的返回&#xff0c;这个过程称之为递推…

EXCEL与数据分析

目录 一、常用技巧 二、数据收集、清洗技巧 三、常用公式 四、常用函数 五、数组 六、查找与引用函数 七、图表 八、数据透视表 九、交互式界面和组合框动态制作 十、录制宏 十一、Power BI&#xff08;商业智能&#xff09; &#xff08;一&#xff09;Power Quer…

自定义配置文件读取产生的“无法添加已属于该配置的 ConfigurationSection”异常解决办法...

最近在编写一个读写自定义配置文件的功能时遇到一个问题&#xff0c;在初始化的时候读入配置显示出来&#xff0c;修改后把配置回存到配置文件&#xff0c;在回存的时候&#xff0c;先移除配置节&#xff0c;再添加&#xff0c;在添加的时候遇到如下的异常&#xff1a; {"…

django.template.exceptions.TemplateSyntaxError: add requires 2 arguments, 1 provided

django.template.exceptions.TemplateSyntaxError: add requires 2 arguments, 1 provided 解决方法&#xff1a; <th>{{page_list.start_index|add:forloop.counter0}}</th>

关于自动化实施过程中思考与感悟(1)

小组内做自动化是从19年开始做的&#xff0c;到目前为止有一年多时间了&#xff0c;中间踩过很多坑&#xff0c;目前还在探索适合当前项目&#xff0c;适合当前公司的一种模式&#xff0c;如何将自动化的效益最大化&#xff0c;帮助小组成员解决项目中实际问题&#xff0c;而不…

Excel进行数据分析数据理解数据清洗构建模型

众所周知&#xff0c;excel是一个强大的办公软件。作为一个统计学专业的学生&#xff0c;一提到数据分析&#xff0c;大家所用的都是python、C、R等语言&#xff0c;却忘了很多基本的工作完全可以在excel里面用更简单的操作完成&#xff0c;尤其是那些对编程头痛的小伙伴&#…

Redis一年降价6成,阿里云已成国内云计算价格杀手?

Redis一年降价6成&#xff0c;阿里云已成国内云计算价格杀手&#xff1f; 近日&#xff0c;阿里云宣布核心产品继续降价。其中&#xff0c;云数据库Redis版&#xff08;集群版&#xff09;价格最高降幅35%&#xff0c;这基本上已经接近6折的价格了&#xff0c;看来阿里云铁心要…

微信小程序周报(第七期)

2019独角兽企业重金招聘Python工程师标准>>> 《桃花庵–程序员版》 写字楼里写字间&#xff0c;写字间中程序员&#xff1b; 程序人员写程序&#xff0c;又将程序换酒钱&#xff1b; 酒醒只在屏前坐&#xff0c;酒醉还来屏下眠&#xff1b; 酒醉酒醒日复日&#xff…

RobotFramework中实现接口上传文件

RF中完成接口用例时&#xff0c;接口需要上传文件&#xff0c;抓包如截图所示&#xff1a; 之前都是将关键字写在py文件的类中&#xff0c;作为关键字导入的&#xff0c;如图所示&#xff1a; 方法是可以实现&#xff0c;但是与其他接口不一致&#xff0c;于是就在RF中直接写&a…

使用Promise链式调用解决多个异步回调的问题

使用Promise链式调用解决多个异步回调的问题 比如我们平常经常遇到的一种情况&#xff1a; 网站中需要先获取用户名&#xff0c;然后再根据用户名去获取用户信息。这里获取用户名getUserName()和获取用户信息getUser()都是调用接口的异步请求。在获取用户信息之前&#xff0c;需…
最新文章