Debounce 和 throttle 是我们在 JavaScript 中使用的两个概念,用于增强对函数执行的控制,这在事件处理程序中特别有用。这两种技术都回答了同一个问题“一段时间内某个函数的调用频率是多少?”

? 相关链接

文中内容多数来自以下文章,侵删!

  • hackll.com/2015/11/19/…
  • github.com/mqyqingfeng…
  • drupalsun.com/david-corba…
  • blog.coding.net/blog/the-di…
  • github.com/jashkenas/u…
  • github.com/jashkenas/u…

? Debounce

1. 概念

  • 本是机械开关的“去弹跳”概念,弹簧开关按下后,由于簧片的作用,接触点会连续接触断开好多次,如果每次接触都通电对用电器不好,所以就要控制按下到稳定的这段时间不通电

  • 前端开发中则是一些频繁的事件触发

    • 鼠标(mousemove...)键盘(keydown...)事件等
    • 表单的实时校验(频繁发送验证请求)
  • 在 debounce 函数没有再被调用的情况下经过 delay 毫秒后才执行回调函数,例如

    • mousemove事件中,确保多次触发只调用一次监听函数
    • 在表单校验的时候,不加防抖,依次输入user,就会分成uususe,user四次发出请求;而添加防抖,设置好时间,可以实现完整输入user才发出校验请求

2. 思路

  • 由 debounce 的功能可知防抖函数至少接收两个参数(流行类库中都是 3 个参数)

    • 回调函数fn
    • 延时时间delay
  • debounce 函数返回一个闭包,闭包被频繁的调用

    • debounce 函数只调用一次,之后调用的都是它返回的闭包函数
    • 在闭包内部限制了回调函数fn的执行,强制只有连续操作停止后执行一次
  • 使用闭包是为了使指向定时器的变量不被gc回收

    • 实现在延时时间delay内的连续触发都不执行回调函数fn,使用的是在闭包内设置定时器setTimeOut
    • 频繁调用这个闭包,在每次调用时都要将上次调用的定时器清除
    • 被闭包保存的变量就是指向上一次设置的定时器

3. 实现

  • 符合原理的简单实现

    function debounce(fn, delay) {var timer;return function() {// 清除上一次调用时设置的定时器// 计时器清零clearTimeout(timer);// 重新设置计时器timer = setTimeout(fn, delay);};
    }
    复制代码
  • 简单实现的代码,可能会造成两个问题

    • this指向问题。debounce 函数在定时器中调用回调函数fn,所以fn执行的时候this指向全局对象(浏览器中window),需要在外层用变量将this保存下来,使用apply进行显式绑定

      function debounce(fn, delay) {var timer;return function() {// 保存调用时的thisvar context = this;clearTimeout(timer);timer = setTimeout(function() {// 修正 this 的指向fn.apply(this);}, delay);};
      }
      复制代码
    • event对象。JavaScript 的事件处理函数中会提供事件对象event,在闭包中调用时需要将这个事件对象传入

      function debounce(fn, delay) {var timer;return function() {// 保存调用时的thisvar context = this;// 保存参数var args = arguments;clearTimeout(timer);timer = setTimeout(function() {console.log(context);// 修正this,并传入参数fn.apply(context, args);}, delay);};
      }
      复制代码

4. 完善(underscore的实现)

  • 立刻执行。增加第三个参数,两种情况

    • 先执行回调函数fn,等到停止触发后的delay毫秒,才可以再次触发(先执行
    • 连续的调用 debounce 函数不触发回调函数,停止调用经过delay毫秒后才执行回调函数(后执行
    • clearTimeout(timer)后,timer并不会变成null,而是依然指向定时器对象
    function debounce(fn, delay, immediate) {var timer;return function() {var context = this;var args = arguments;// 停止定时器if (timer) clearTimeout(timer);// 回调函数执行的时机if (immediate) {// 是否已经执行过// 执行过,则timer指向定时器对象,callNow 为 false// 未执行,则timer 为 null,callNow 为 truevar callNow = !timer;// 设置延时timer = setTimeout(function() {timer = null;}, delay);if (callNow) fn.apply(context, args);} else {// 停止调用后delay时间才执行回调函数timer = setTimeout(function() {fn.apply(context, args);}, delay);}};
    }
    复制代码
  • 返回值与取消 debounce 函数

    • 回调函数可能有返回值。

      • 后执行情况可以不考虑返回值,因为在执行回调函数前的这段时间里,返回值一直是undefined
      • 先执行情况,会先得到返回值
    • 能取消 debounce 函数。一般当immediatetrue的时候,触发一次后要等待delay时间后才能再次触发,但是想要在这个时间段内想要再次触发,可以先取消掉之前的 debounce 函数
    function debounce(fn, delay, immediate) {var timer, result;var debounced = function() {var context = this;var args = arguments;// 停止定时器if (timer) clearTimeout(timer);// 回调函数执行的时机if (immediate) {// 是否已经执行过// 执行过,则timer指向定时器对象,callNow 为 false// 未执行,则timer 为 null,callNow 为 truevar callNow = !timer;// 设置延时timer = setTimeout(function() {timer = null;}, delay);if (callNow) result = fn.apply(context, args);} else {// 停止调用后delay时间才执行回调函数timer = setTimeout(function() {fn.apply(context, args);}, delay);}// 返回回调函数的返回值return result;};// 取消操作debounced.cancel = function() {clearTimeout(timer);timer = null;};return debounced;
    }
    复制代码
  • ES6 写法

    function debounce(fn, delay, immediate) {let timer, result;// 这里不能使用箭头函数,不然 this 依然会指向 Windows对象// 使用rest参数,获取函数的多余参数const debounced = function(...args) {if (timer) clearTimeout(timer);if (immediate) {const callNow = !timer;timer = setTimeout(() => {timer = null;}, delay);if (callNow) result = fn.apply(this, args);} else {timer = setTimeout(() => {fn.apply(this, args);}, delay);}return result;};debounced.cancel = () => {clearTimeout(timer);timer = null;};return debounced;
    }
    复制代码

? throttle

1. 概念

  • 固定函数执行的速率

  • 如果持续触发事件,每隔一段时间,执行一次事件

    • 例如监听mousemove事件时,不管鼠标移动的速度,【节流】后的监听函数会在 wait 秒内最多执行一次,并以此【匀速】触发执行
  • windowresizescroll事件的优化等

2. 思路

  • 有两种主流实现方式

    • 使用时间戳
    • 设置定时器
  • 节流函数 throttle 调用后返回一个闭包

    • 闭包用来保存之前的时间戳或者定时器变量(因为变量被返回的函数引用,所以无法被垃圾回收机制回收
  • 时间戳方式

    • 当触发事件的时候,取出当前的时间戳,然后减去之前的时间戳(初始设置为 0)
    • 结果大于设置的时间周期,则执行函数,然后更新时间戳为当前时间戳
    • 结果小于设置的时间周期,则不执行函数
  • 定时器方式

    • 当触发事件的时候,设置一个定时器
    • 再次触发事件的时候,如果定时器存在,就不执行,知道定时器执行,然后执行函数,清空定时器
    • 设置下个定时器
  • 将两种方式结合,可以实现兼并立刻执行和停止触发后依然执行一次的效果

3. 实现

  • 时间戳实现

    function throttle(fn, wait) {var args;// 前一次执行的时间戳var previous = 0;return function() {// 将时间转为时间戳var now = +new Date();args = arguments;// 时间间隔大于延迟时间才执行if (now - previous > wait) {fn.apply(this, args);previous = now;}};
    }
    复制代码
    • 触发监听事件,回调函数会立刻执行(初始的previous为 0,除非设置的时间间隔大于当前时间的时间戳,否则差值肯定大于时间间隔)
    • 停止触发后,无论停止时间在哪,都不会再执行。例如,1 秒执行 1 次,在 4.2 秒停止,则第 5 秒不会再执行 1 次
  • 定时器实现

    function throttle(fn, wait) {var timer, context, args;return function() {context = this;args = arguments;// 如果定时器存在,则不执行if (!timer) {timer = setTimeout(function() {// 执行后释放定时器变量timer = null;fn.apply(context, args);}, wait);}};
    }
    复制代码
    • 回调函数不会立刻执行,要在 wait 秒后第一次执行,停止触发闭包后,如果停止时间在两次执行之间,则还会执行一次
  • 结合时间戳和定时器实现

    function throttle(fn, wait) {var timer, context, args;var previous = 0;// 延时执行函数var later = function() {previous = +new Date();// 执行后释放定时器变量timer = null;fn.apply(context, args);if (!timeout) context = args = null;};var throttled = function() {var now = +new Date();// 距离下次执行 fn 的时间// 如果人为修改系统时间,可能出现 now 小于 previous 情况// 则剩余时间可能超过时间周期 waitvar remaining = wait - (now - previous);context = this;args = arguments;// 没有剩余时间 || 修改系统时间导致时间异常,则会立即执行回调函数fn// 初次调用时,previous为0,除非wait大于当前时间的时间戳,否则剩余时间一定小于0if (remaining <= 0 || remaining > wait) {// 如果存在延时执行定时器,将其取消掉if (timer) {clearTimeout(timer);timer = null;}previous = now;fn.apply(context, args);if (!timeout) context = args = null;} else if (!timer) {// 设置延时执行timer = setTimeout(later, remaining);}};return throttled;
    }
    复制代码
    • 过程中的节流功能是由时间戳的原理实现,同时实现了立刻执行
    • 定时器只是用来设置在最后退出时增加一个延时执行
    • 定时器在每次触发时都会重新计时,但是只要不停止触发,就不会去执行回调函数 fn

4. 优化完善

  • 增加第三个参数,让用户可以自己选择模式

    • 忽略开始边界上的调用,传入{ leading: false }
    • 忽略结尾边界上的调用,传入{ trailing: false }
  • 增加返回值功能

  • 增加取消功能

    function throttle(func, wait, options) {var context, args, result;var timeout = null;// 上次执行时间点var previous = 0;if (!options) options = {};// 延迟执行函数var later = function() {// 若设定了开始边界不执行选项,上次执行时间始终为0previous = options.leading === false ? 0 : new Date().getTime();timeout = null;// func 可能会修改 timeout 变量result = func.apply(context, args);// 定时器变量引用为空,表示最后一次执行,则要清除闭包引用的变量if (!timeout) context = args = null;};var throttled = function() {var now = new Date().getTime();// 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。if (!previous && options.leading === false) previous = now;// 延迟执行时间间隔var remaining = wait - (now - previous);context = this;args = arguments;// 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口// remaining 大于时间窗口 wait,表示客户端系统时间被调整过if (remaining <= 0 || remaining > wait) {if (timeout) {clearTimeout(timeout);timeout = null;}previous = now;result = func.apply(context, args);if (!timeout) context = args = null;} else if (!timeout && options.trailing !== false) {timeout = setTimeout(later, remaining);}// 返回回调函数执行后的返回值return result;};throttled.cancel = function() {clearTimeout(timeout);previous = 0;timeout = context = args = null;};return throttled;
    }
    复制代码
    • 有个问题,leading: falsetrailing: false 不能同时设置

      • 第一次开始边界不执行,但是,第一次触发时,previous为 0,则remaining值和wait相等。所以,if (!previous && options.leading === false)为真,改变了previous的值,而if (remaining <= 0 || remaining > wait)为假
      • 以后再触发就会导致if (!previous && options.leading === false)为假,而if (remaining <= 0 || remaining > wait)为真。就变成了开始边界执行。这样就和leading: false冲突了

? 总结

  • 至此,完整实现了一个underscore中的 debounce 函数和 throttle 函数
  • lodash中 debounce 函数和 throttle 函数的实现更加复杂,封装更加彻底
  • 推荐两个可视化执行过程的工具
    • demo.nimius.net/debounce_th…
    • caiogondim.github.io/js-debounce…
  • 自己实现是为了学习其中的思想,实际开发中尽量使用 lodash 或 underscore 这样的类库。

对比

  • throttle 和 debounce 是解决请求和响应速度不匹配问题的两个方案。二者的差异在于选择不同的策略

  • 电梯超时现象解释两者区别。假设电梯设定为 15 秒,不考虑容量限制

    • throttle策略:保证如果电梯第 1 个人进来后,15 秒后准时送一次,不等待。如果没有人,则待机、
    • debounce策略:如果电梯有人进来,等待 15 秒,如果又有人进来,重新计时 15 秒,直到 15 秒超时都没有人再进来,则开始运送

节流与防抖【从0到0.1】相关推荐

  1. vue 接口节流_vue防抖节流之v-debounce--throttle使用指南

    最新封装了一个vue防抖节流自定义指令,发布到npm上,有用欢迎star,谢谢! 使用比较简单,取消利用vue注册事件,采用指令来注册事件,防抖指令v-debounce,节流指令v-debounce, ...

  2. java接口防抖_彻底弄懂节流和防抖

    节流和防抖 这两个东西,你肯定听过,就是两种优化浏览器性能的手段.相关文章你肯定也看过,如果还是不太清楚,没关系,看完这篇短文,相信你能轻松理解其中差别. 防抖(deounce) 我们先说防抖吧,这里 ...

  3. Apache Hudi 0.7.0 和 0.8.0 新功能已在 Amazon EMR 中可用

    文末限时福利倒计时3天,不要错过! 前言 Apache Hudi 是一个开源事务性数据湖框架,通过提供记录级插入.更新和删除功能,极大地简化了增量数据处理和数据管道开发.如果您要在 Amazon Si ...

  4. libgstreamer-1.0.so.0: cannot open shared object file: No such file or directory

    1. 问题现象 error while loading shared libraries: libgstreamer-1.0.so.0: cannot open shared object file: ...

  5. c+语言+null,C/C++语言中NULL、'\0’和0的区别

    NULL.'\0'和0的值是一样的,都是0,不过它们的表现形式不一样: 1. NULL: 即空指针,不过在C和C++中并不一样.在VS 2013的库文件string.h中可以看到如果定义. 1 /* ...

  6. Ubuntu14.04 64位机上安装cuda8.0+cudnn5.0操作步骤

    查看Ubuntu14.04 64位上显卡信息,执行: lspci | grep -i vga lspci -v -s 01:00.0 nvidia-smi 第一条此命令可以显示一些显卡的相关信息:如果 ...

  7. Spring Cloud Alibaba 基础教程:Nacos 生产级版本 0.8.0

    Spring Cloud Alibaba 基础教程:Nacos 生产级版本 0.8.0 昨晚Nacos社区发布了第一个生产级版本:0.8.0.由于该版本除了Bug修复之外,还提供了几个生产管理非常重要 ...

  8. Silverlight 3发布新版3.0.50106.0

    微软1月19日发布Silverlight 3新版本3.0.50106.0. 该版本主要修复以下几个问题: 问题一: 当使用图形硬件加速功能(GPU)的时候,如果GPU驱动报错,Silverlight ...

  9. AS1.0(2.0)中的XML示例

    虽然Flash早就升级为AS3.0,但是FMS的服务端编程依然仅支持AS1.0(2.0),服务端与.net通讯的最简单方式莫过于请求一个RESTful的webService或wcf,通过它们返回的xm ...

  10. 多数编程语言里的0.1+0.2≠0.3?

    作者 | Parul Malhotra 译者 | Raku 出品 | AI科技大本营(ID:rgznai100) 我们从小就被教导说0.1+0.2=0.3,但是在奇妙的计算机编程世界里面,事情变得不一 ...

最新文章

  1. R语言ggplot2使用geom_line函数geom_point函数可视化哑铃图、并对哑铃图进行排序(reorder dumbbell plot)
  2. python笔记基础-python学习笔记之基础一(第一天)
  3. 关于RasASM的一个编译错误
  4. php的数据校验,php 数据类型校验函数的简单示例
  5. PyQt的QTableWidget的全面总结与归纳
  6. 二叉树路径和最大python_python3实现在二叉树中找出和为某一值的所有路径(推荐)...
  7. ucosii任务堆栈的作用是什么呢?
  8. YAFFS2文件系统在嵌入式LINUX系统中的应用
  9. cdn加载vue很慢_Vue.js 项目打包优化实践
  10. 百度 (baidu) 举办了一场全公司范围内的 拳皇友谊赛
  11. Winform用Post方式打开IE
  12. Spring MVC url提交参数和获取参数
  13. 怎样用ZBrush中的Curves和Insert笔刷创建四肢
  14. 高级php面试题(转)
  15. np.array(image)的作用
  16. mysql什么情况下死锁_2020-07-08:mysql只有一个表a,什么情况下会造成死锁,解决办法是什么?...
  17. jrtplib 编译安装配置
  18. w10怎么自动锁定计算机,教你如何设置Win10系统自动锁屏?
  19. linux 内核udp编程,[求助]linux内核代码udp_recvmsg()函数中的代码绕过问题。
  20. 常见的影视cms及安装环境说明

热门文章

  1. 论文解读 Combating Adversarial Misspellings with Robust Word Recognition
  2. BatchNormalization对cnn训练的影响
  3. 宝塔mysql优化_宝塔面板下实现MySQL性能优化处理
  4. PHP Include 文件
  5. 备战2022秋季“金三银四”跳槽必备:软件测试面试题,贡献给需要的小伙伴,最后有惊喜哦
  6. 计算机录入技能考试题,计算机文字录入员高级技能考试试卷
  7. java正则表达式的用法_Java 正则表达式的使用
  8. MyISAM与InnoDB区别
  9. mybatis delete返回值_从零开始学习在IntelliJ IDEA 中使用mybatis
  10. katalon进行app测试_使用Katalon Studio创建你的第一个API测试