Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群

作者丨highway

来源丨iOS成长之路

Throttle和Debounce是什么

Throttle本是机械领域的概念,英文解释为:

A valve that regulates the supply of fuel to the engine.

中文翻译成节流器,用以调节发动机燃料供应的阀门。在计算机领域,同样也引入了Throttle和Debounce概念,这两种技术都可用来降低函数调用频率,相似又有区别。对于连续调用的函数,尤其是触发频率密集、目标函数涉及大量计算时,恰当使用Throttle和Debounce可以有效提升性能及系统稳定性。

对于JS前端开发人员,由于无法控制DOM事件触发频率,在给DOM绑定事件的时候,常常需要进行Throttle或者Debounce来防止事件调用过于频繁。而对于iOS开发者来说,也许会觉得这两个术语很陌生,不过你很可能在不经意间已经用到了,只是没想过会有专门的抽象概念。举个常见的例子,对于UITableView,频繁触发reloadData函数可能会引起画面闪动、卡顿,数据源动态变化时甚至会导致崩溃,一些开发者可能会想方设法减少对reload函数的调用,不过对于复杂的UITableView视图可能会显得捉襟见肘,因为reloadData很可能“无处不在”,甚至会被跨文件调用,此时就可以考虑对reloadData函数本身做下降频处理。

下面通过概念定义及示例来详细解析对比下Throttle和Debounce,先看下二者在JS的Lodash库中的解释:

Throttle

Throttle enforces a maximum number of times a function can be called over time. For example, "execute this function at most once every 100 ms."

即,Throttle使得函数在规定时间间隔内(如100 ms),最多只能调用一次。

Debounce

Debounce enforces that a function not be called again until a certain amount of time has passed without it being called. For example, "execute this function only if 100 ms have passed without it being called."

即,Debounce可以将小于规定时间间隔(如100 ms)内的函数调用,归并成一次函数调用。

对于Debounce的理解,可以想象一下电梯的例子。你在电梯中,门快要关了,突然又有人要进来,电梯此时会再次打开门,直到短时间内没有人再进为止。虽然电梯上行下行的时间延迟了,但是优化了整体资源配置。

我们再以拖拽手势回调的动图展示为例,来直观感受下Throttle和Debounce的区别。每次“walk me”图标拖拽时,会产生一次回调。在动图的右上角,可以看到回调函数实际调用的次数。

概念演示

1)正常回调:

正常回调

2)Throttle(Leading)模式下的回调:

Throttle(Leading)模式下的回调

3)Debounce(Trailing)模式下的回调:

Debounce(Trailing)模式下的回调

应用场景

以下是几个典型的Throttle和Debounce应用场景。

防止按钮重复点击

为了防止用户重复快速点击,导致冗余的网络请求、动画跳转等不必要的损耗,可以使用Throttle的Leading模式,只响应指定时间间隔内的第一次点击。

滚动拖拽等密集事件

可以在UIScrollView的滚动回调didScroll函数里打日志观察下,调用频率相当高,几乎每移动1个像素都可能产生一次回调,如果回调函数的计算量偏大很可能会导致卡顿,此种情况下就可以考虑使用Throttle降频。

文本输入自动完成

假如想要实现,在用户输入时实时展示搜索结果,常规的做法是用户每改变一个字符,就触发一次搜索,但此时用户很可能还没有输入完成,造成资源浪费。此时就可以使用Debounce的Trailing模式,在字符改变之后的一段时间内,用户没有继续输入时,再触发搜索动作,从而有效节省网络请求次数。

数据同步

以用户埋点日志上传为例,没必要在用户每操作一次后就触发一次网络请求,此时就可以使用Debounce的Traling模式,在记录用户开始操作之后,且一段时间内不再操作时,再把日志merge之后上传至服务端。其他类似的场景,比如客户端与服务端版本同步,也可以采取这种策略。

在系统层面,或者一些知名的开源库里,也经常可以看到Throttle或者Debounce的身影。

GCD Background Queue

Items dispatched to the queue run at background priority; the queue is scheduled for execution after all high priority queues have been scheduled and the system runs items on a thread whose priority is set for background status. Such a thread has the lowest priority and any disk I/O is throttled to minimize the impact on the system. 在dispatch的Background Queue优先级下,系统会自动将磁盘I/O操作进行Throttle,来降低对系统资源的耗费。

ASIHttpRequest及AFNetworking

- (void)handleNetworkEvent:(CFStreamEventType)type
{//...[self performThrottling];//...
}
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytesdelay:(NSTimeInterval)delay;

在弱网环境下, 一个Packet一次传输失败的概率会升高,由于TCP是有序且可靠的,前一个Packet不被ack的情况下,后面的Packet就要等待,所以此时如果启用Network Throttle机制,减小写入数据量,反而会提升网络请求的成功率。

iOS实现

理解了Throttle和Debounce的概念后,在单个业务场景中实现起来是很容易的事情,但是考虑到其应用如此广泛,就应该封装成为业务无关的组件,减小重复劳动,提升开发效率。

前文提过,Throttle和Debounce在Web前端已经有相当成熟的实现,Ben Alman之前做过一个JQuery插件(不再维护),一年后Jeremy Ashkenas把它加入了underscore.js,而后又加入了Lodash[1]。但是在iOS开发领域,尤其是对于Objective-C语言,尚且没有一个可靠、稳定且全面的第三方库。

杨萧玉曾经开源过一个 MessageThrottle[2] 库,该库使用Objective-C的runtime与消息转发机制,使用非常便捷。但是这个库的缺点也比较明显,使用了大量的底层HOOK方法,系统兼容性方面还需要进一步的验证和测试,如果集成的项目中同时使用了其他使用底层runtime的方法,可能会产生冲突,导致非预期后果。另外该库是完全面向切面的,作用于全局且隐藏较深,增加了一定的调试成本。为此笔者封装了一个新的实现HWThrottle,并借鉴了Lodash的接口及实现方式,该库有以下特点:

1、未使用任何runtime API,全部由顶层API实现;

2、每个业务场景需要使用者自己定义一个实例对象,自行管理生命周期,旨在把对项目的影响控制在最小范围;

3、区分Throttle和Debounce,提供Leading和Trailing选项。

Demo

下面展示了对按钮点击事件进行Throttle或Debounce的效果,click count表示点击按钮次数,call count表示实际调用目标事件的次数。

在leading模式下,会在指定时间间隔的开始处触发调用;Trailing模式下,会在指定时间间隔的末尾处触发调用。

1) Throttle Leading

Throttle Leading

2) Throttle Trailing

Throttle Trailing

3) Debounce Trailing

Debounce Trailing

4) Debounce Leading

Debounce Leading

使用示例:

if (!self.testThrottler) {self.testThrottler = [[HWThrottle alloc] initWithThrottleMode:HWThrottleModeLeadinginterval:1onQueue:dispatch_get_main_queue()taskBlock:^{//do some heavy tasks}];
}
[self.testThrottler call];

由于使用到了block,注意在Throttle或Debounce对象所有者即将释放时,即不再使用block时调用invalidate,该方法会将持有的task block置空,防止循环引用。如果是在页面中使用Throttle或Debounce对象,可在disappear回调中调用invalidate方法。

- (void)viewWillDisappear:(BOOL)animated {[super viewWillDisappear:animated];[self.testThrottler invalidate];
}

接口API:

HWThrottle.h:

#pragma mark - public classtypedef NS_ENUM(NSUInteger, HWThrottleMode) {HWThrottleModeLeading,          //invoking on the leading edge of the timeoutHWThrottleModeTrailing,         //invoking on the trailing edge of the timeout
};typedef void(^HWThrottleTaskBlock)(void);@interface HWThrottle : NSObject/// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading, the execution queue defaults to the main queue. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other
/// @param interval Throttle time interval, unit second
/// @param taskBlock The task to be throttled
- (instancetype)initWithInterval:(NSTimeInterval)intervaltaskBlock:(HWThrottleTaskBlock)taskBlock;/// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other
/// @param interval Throttle time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be throttled
- (instancetype)initWithInterval:(NSTimeInterval)intervalonQueue:(dispatch_queue_t)queuetaskBlock:(HWThrottleTaskBlock)taskBlock;/// Initialize a debounce object. Note that debounce is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other
/// @param throttleMode The throttle mode, defaults HWThrottleModeLeading
/// @param interval Throttle time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be throttled
- (instancetype)initWithThrottleMode:(HWThrottleMode)throttleModeinterval:(NSTimeInterval)intervalonQueue:(dispatch_queue_t)queuetaskBlock:(HWThrottleTaskBlock)taskBlock;/// throttling call the task
- (void)call;/// When the owner of the HWThrottle object is about to release, call this method on the HWThrottle object first to prevent circular references
- (void)invalidate;@end

Throttle默认模式为Leading,因为实际使用中,多数的Throttle场景是在指定时间间隔的开始处调用,比如防止按钮重复点击时,一般会响应第一次点击,而忽略之后的点击。

HWDebounce.h:

#pragma mark - public classtypedef NS_ENUM(NSUInteger, HWDebounceMode) {HWDebounceModeTrailing,        //invoking on the trailing edge of the timeoutHWDebounceModeLeading,         //invoking on the leading edge of the timeout
};typedef void(^HWDebounceTaskBlock)(void);@interface HWDebounce : NSObject/// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing, the execution queue defaults to the main queue. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other
/// @param interval Debounce time interval, unit second
/// @param taskBlock The task to be debounced
- (instancetype)initWithInterval:(NSTimeInterval)intervaltaskBlock:(HWDebounceTaskBlock)taskBlock;/// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other
/// @param interval Debounce time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be debounced
- (instancetype)initWithInterval:(NSTimeInterval)intervalonQueue:(dispatch_queue_t)queuetaskBlock:(HWDebounceTaskBlock)taskBlock;/// Initialize a debounce object. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other
/// @param debounceMode The debounce mode, defaults HWDebounceModeTrailing
/// @param interval Debounce time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be debounced
- (instancetype)initWithDebounceMode:(HWDebounceMode)debounceModeinterval:(NSTimeInterval)intervalonQueue:(dispatch_queue_t)queuetaskBlock:(HWDebounceTaskBlock)taskBlock;/// debouncing call the task
- (void)call;/// When the owner of the HWDebounce object is about to release, call this method on the HWDebounce object first to prevent circular references
- (void)invalidate;@end

Debounce默认模式为Trailing,因为实际使用中,多数的Debounce场景是在指定时间间隔的末尾处调用,比如监听用户输入时,一般是在用户停止输入后再触发调用。

核心代码:

Throttle leading:

- (void)call {if (self.lastRunTaskDate) {if ([[NSDate date] timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) {[self runTaskDirectly];}} else {[self runTaskDirectly];}
}- (void)runTaskDirectly {dispatch_async(self.queue, ^{if (self.taskBlock) {self.taskBlock();}self.lastRunTaskDate = [NSDate date];});
}- (void)invalidate {self.taskBlock = nil;
}

Throttle trailing:

- (void)call {NSDate *now = [NSDate date];if (!self.nextRunTaskDate) {if (self.lastRunTaskDate) {if ([now timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) {self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now];} else {self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:self.lastRunTaskDate];}} else {self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now];}NSTimeInterval nextInterval = [self.nextRunTaskDate timeIntervalSinceDate:now];dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextInterval * NSEC_PER_SEC)), self.queue, ^{if (self.taskBlock) {self.taskBlock();}self.lastRunTaskDate = [NSDate date];self.nextRunTaskDate = nil;});}
}- (void)invalidate {self.taskBlock = nil;
}

Debounce trailing:

- (void)call {if (self.block) {dispatch_block_cancel(self.block);}__weak typeof(self)weakSelf = self;self.block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{if (weakSelf.taskBlock) {weakSelf.taskBlock();}});dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.interval * NSEC_PER_SEC)), self.queue, self.block);
}- (void)invalidate {self.taskBlock = nil;self.block = nil;
}

Debounce leading:

- (void)call {if (self.lastCallTaskDate) {if ([[NSDate date] timeIntervalSinceDate:self.lastCallTaskDate] > self.interval) {[self runTaskDirectly];}} else {[self runTaskDirectly];}self.lastCallTaskDate = [NSDate date];
}- (void)runTaskDirectly {dispatch_async(self.queue, ^{if (self.taskBlock) {self.taskBlock();}});
}- (void)invalidate {self.taskBlock = nil;self.block = nil;
}

总结

希望此篇文章能帮助你全面理解Throttle和Debounce的概念,赶快看看项目中有哪些可以用到Throttle或Debounce来提升性能的地方吧。

再次附上OC实现HWThrottle[3],欢迎issue和讨论。

参考资料

[1]

Lodash documentation: https://lodash.com/docs/

[2]

MessageThrottle: https://github.com/yulingtianxia/MessageThrottle

[3]

HWThrottle: https://github.com/HighwayLaw/HWThrottle

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

近期精彩内容推荐:  

 华为正式宣布养猪,网友:支持华为自救!

 入职腾讯第九年,我辞职了!

 Windows蓝屏为什么是蓝底白字?

 955 互联网公司白名单来了!

在看点这里好文分享给更多人↓↓

函数节流(Throttle)和防抖(Debounce)解析及其OC实现相关推荐

  1. JavaScript 函数节流 throttle 和防抖 debounce

    今天和别人聊到JavaScript函数的节流和防抖,发现自己对这两个的区别很是模糊,遂小小实践一下,在此记录,希望对需要的人有所帮助. 节流 - 频繁操作,间隔一定时间去做一件事 举例说明:假定时间间 ...

  2. 事件触发控制_前端性能优化:事件的节流throttle与防抖debounce

    scroll 事件是一个非常容易被反复触发的事件,另外,resize 事件.鼠标事件(比如 mousemove.mouseover 等).键盘事件(keyup.keydown 等)都存在被频繁触发的风 ...

  3. 节流(Throttle)和防抖(Debounce)

    为啥要使用防抖和节流 在项目开发中,我们经常用到的滚动事件或者用户输入事件,都是一些高频事件,如果对这类事件触发的频率没有节制,就会加重浏览器和服务器的负担.节流和防抖目的就是减少事件触发的次数. 1 ...

  4. 【前端帮帮忙】第7期 关于节流(throttle)和防抖(debounce)的理解

    节流和防抖在我们平时的项目中挺常用的,也是面试中经常会被提问的知识点,今天我们一起来学习一下. 节流 简单理解就是:控制函数每隔n秒执行一次. 作用 防止用户高频率的触发事件,刚好这个事件又需要处理大 ...

  5. SAP UI5和Angular的函数防抖(Debounce)和函数节流(Throttle)实现原理介绍

    这是Jerry 2021年的第 11 篇文章,也是汪子熙公众号总共第 282 篇原创文章. Jerry之前的文章 SAP UI5 OData谣言粉碎机:极短时间内发送两个Odata request, ...

  6. 函数防抖Debounce和函数节流Throttle

    函数节流 & 函数防抖 函数节流和函数防抖 函数节流和函数防抖二者很容易被混淆起来.下面贴英文原文,建议认真阅读: Debouncing enforces that a function no ...

  7. JavaScript函数节流(throttle)与函数去抖(debounce)解析与应用举例

    概念 函数防抖(debounce): 事件响应函数在一段时间后才会执行,如果在这段时间内再次调用,则重新计算执行时间:当预定时间内没有再次调用该函数,则执行响应逻辑. 函数节流(throttle): ...

  8. javascript 函数节流 throttle 解决函数被频繁调用、浏览器卡顿的问题

    * 使用setTimeout index.html <html> <head><meta charset="UTF-8"><title&g ...

  9. SAP UI5函数节流(Throttle)的一个最简单的例子

    SAP UI5源代码: <!DOCTYPE html> <html> <head><meta http-equiv="X-UA-Compatible ...

  10. 函数防抖(debounce)和节流(throttle)在H5编辑器项目中的应用

    函数防抖(debounce)和节流(throttle)在H5编辑器项目中的应用 文章目录 函数防抖(debounce)和节流(throttle)在H5编辑器项目中的应用 1. 为什么要防抖节流 1.1 ...

最新文章

  1. c语言定义链式队列用菜单,数据结构之---C语言实现链式队列
  2. 必看!互联网开发模式的经验之谈
  3. Visual Studio 远程调试正在运行的进程
  4. 将ubuntu配置为路由器_“名酒为王”时代来临但资源将尽,看泸州老窖如何进行“名酒资源再配置”丨深度观察...
  5. 计算机硬件无法启动不能读取文件,修复Windows出现的“文件或目录已损坏且无法读取”问题...
  6. 小白Linux入门之:常用命令介绍
  7. 在线动态几何编辑器 GeometryEditor
  8. 阜阳智慧城市建设居全国地市级城市第11位
  9. Idea的JShell Console
  10. 收货地址列表html,收货地址.html
  11. stata--异方差检验
  12. 手机UC浏览器导出书签到电脑Chrome浏览器中
  13. JETT(三)-多Sheet渲染
  14. 南宁计算机职称考试网,南宁人事考试职称网
  15. Db2插入数据溢出报错测试
  16. 【行研报告】小红书红人及商业内容数据报告—附下载链接
  17. 【广度优先搜索-中等】1905. 统计子岛屿
  18. 读书的理由——好书推荐
  19. 阿里云如何安装部署Oracle11g 图文?
  20. [11.7日学习记录]多目标优化和帕累托最优解集(未完成)

热门文章

  1. java实现地图导航功能吗_Java web实现百度地图导航
  2. 在线问答系统---数据库管理
  3. 你想要拥有自己的搜索引擎吗?
  4. HaaS轻应用(JavaScript)低功耗蓝牙案例
  5. 【Latex】TexLive安装说明
  6. java script error_java script error 错误解决方法
  7. java 抽象final_java抽象、接口 和final
  8. css 设置容器高度等于宽度,设置容器的宽高一致。
  9. audio音频播放标签样式优化自定义
  10. 插入图片与背景图片的区别