节流与防抖【从0到0.1】
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
,就会分成u
,us
,use
,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 函数。一般当
immediate
为true
的时候,触发一次后要等待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 秒内最多执行一次,并以此【匀速】触发执行
- 例如监听
window
的resize
、scroll
事件的优化等
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: false
和trailing: 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】相关推荐
- vue 接口节流_vue防抖节流之v-debounce--throttle使用指南
最新封装了一个vue防抖节流自定义指令,发布到npm上,有用欢迎star,谢谢! 使用比较简单,取消利用vue注册事件,采用指令来注册事件,防抖指令v-debounce,节流指令v-debounce, ...
- java接口防抖_彻底弄懂节流和防抖
节流和防抖 这两个东西,你肯定听过,就是两种优化浏览器性能的手段.相关文章你肯定也看过,如果还是不太清楚,没关系,看完这篇短文,相信你能轻松理解其中差别. 防抖(deounce) 我们先说防抖吧,这里 ...
- Apache Hudi 0.7.0 和 0.8.0 新功能已在 Amazon EMR 中可用
文末限时福利倒计时3天,不要错过! 前言 Apache Hudi 是一个开源事务性数据湖框架,通过提供记录级插入.更新和删除功能,极大地简化了增量数据处理和数据管道开发.如果您要在 Amazon Si ...
- 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: ...
- c+语言+null,C/C++语言中NULL、'\0’和0的区别
NULL.'\0'和0的值是一样的,都是0,不过它们的表现形式不一样: 1. NULL: 即空指针,不过在C和C++中并不一样.在VS 2013的库文件string.h中可以看到如果定义. 1 /* ...
- Ubuntu14.04 64位机上安装cuda8.0+cudnn5.0操作步骤
查看Ubuntu14.04 64位上显卡信息,执行: lspci | grep -i vga lspci -v -s 01:00.0 nvidia-smi 第一条此命令可以显示一些显卡的相关信息:如果 ...
- Spring Cloud Alibaba 基础教程:Nacos 生产级版本 0.8.0
Spring Cloud Alibaba 基础教程:Nacos 生产级版本 0.8.0 昨晚Nacos社区发布了第一个生产级版本:0.8.0.由于该版本除了Bug修复之外,还提供了几个生产管理非常重要 ...
- Silverlight 3发布新版3.0.50106.0
微软1月19日发布Silverlight 3新版本3.0.50106.0. 该版本主要修复以下几个问题: 问题一: 当使用图形硬件加速功能(GPU)的时候,如果GPU驱动报错,Silverlight ...
- AS1.0(2.0)中的XML示例
虽然Flash早就升级为AS3.0,但是FMS的服务端编程依然仅支持AS1.0(2.0),服务端与.net通讯的最简单方式莫过于请求一个RESTful的webService或wcf,通过它们返回的xm ...
- 多数编程语言里的0.1+0.2≠0.3?
作者 | Parul Malhotra 译者 | Raku 出品 | AI科技大本营(ID:rgznai100) 我们从小就被教导说0.1+0.2=0.3,但是在奇妙的计算机编程世界里面,事情变得不一 ...
最新文章
- R语言ggplot2使用geom_line函数geom_point函数可视化哑铃图、并对哑铃图进行排序(reorder dumbbell plot)
- python笔记基础-python学习笔记之基础一(第一天)
- 关于RasASM的一个编译错误
- php的数据校验,php 数据类型校验函数的简单示例
- PyQt的QTableWidget的全面总结与归纳
- 二叉树路径和最大python_python3实现在二叉树中找出和为某一值的所有路径(推荐)...
- ucosii任务堆栈的作用是什么呢?
- YAFFS2文件系统在嵌入式LINUX系统中的应用
- cdn加载vue很慢_Vue.js 项目打包优化实践
- 百度 (baidu) 举办了一场全公司范围内的 拳皇友谊赛
- Winform用Post方式打开IE
- Spring MVC url提交参数和获取参数
- 怎样用ZBrush中的Curves和Insert笔刷创建四肢
- 高级php面试题(转)
- np.array(image)的作用
- mysql什么情况下死锁_2020-07-08:mysql只有一个表a,什么情况下会造成死锁,解决办法是什么?...
- jrtplib 编译安装配置
- w10怎么自动锁定计算机,教你如何设置Win10系统自动锁屏?
- linux 内核udp编程,[求助]linux内核代码udp_recvmsg()函数中的代码绕过问题。
- 常见的影视cms及安装环境说明
热门文章
- 论文解读 Combating Adversarial Misspellings with Robust Word Recognition
- BatchNormalization对cnn训练的影响
- 宝塔mysql优化_宝塔面板下实现MySQL性能优化处理
- PHP Include 文件
- 备战2022秋季“金三银四”跳槽必备:软件测试面试题,贡献给需要的小伙伴,最后有惊喜哦
- 计算机录入技能考试题,计算机文字录入员高级技能考试试卷
- java正则表达式的用法_Java 正则表达式的使用
- MyISAM与InnoDB区别
- mybatis delete返回值_从零开始学习在IntelliJ IDEA 中使用mybatis
- katalon进行app测试_使用Katalon Studio创建你的第一个API测试