函数防抖与节流不是新概念,在前端领域很常见,也是面试中的常客,搜索"前端 函数防抖"能看到很多文章。
相反,在 iOS 上却看不到很多介绍。
第一次知道 函数防抖与节流
,是在 2018 年做交易所项目:
当时的场景,是实时更新交易数据。
交易订单数据变化很频繁,每次都去刷新,显然不是一个好方法。
而且不能直接丢数据,常规的"第一次执行,后续丢弃"的限频策略,满足不了需求。
当时思考,这个策略应满足的条件:
因为代码实现问题,和大佬请教。
说明完目的,他一听就说,这不是函数防抖和节流吗?在前端很常见..
好嘛...原来人家前端早就有了?我都工作 2 年里才知道,又学会了新姿势,好饭不怕晚。
而我发现这个概念,不仅是前端,后端也能应用,甚至 TCP 的流量控制策略,就是属于函数防抖。
前面解释了为什么
要用到 函数防抖和节流
,现在说说它们具体是什么。
很多文章都提到一个演示的网址 debounce&throttle,里面模拟鼠标移动事件几种情况的调用,带颜色的竖线,代表一次函数执行。
使用的大概效果是这样:
regular
代表常规情况,不做限制时,函数直接调用的结果。deboundce
代表防抖,可以发现,如果函数一直调用,它不会立即执行,而是等到一段时间后,函数没有新调用,它才执行一次。throttle
代表节流,在一定时间内,只执行一次防抖的情况,有点像一个极度珍惜 执行机会的人,只要时间段内,有任务来,就再等一会。
等到最后一次,超过一定时间,确定没有新任务了,才去做执行。
有人觉得它像黑车司机,有人形容它是上班时的电梯,但黑车或者电梯容量满了都会开走。
而我认为,它就像一只耐心上好的怪兽,等到所有食物都来完了,确定没有新食物,再张开它的大嘴,一网打尽。
节流比较好理解,在一定时间段内,丢弃掉其它触发,就做一次执行。
函数节流
的使用场景:
其实函数节流
最简单的实现方式,仅用时间戳对比,就可以办到,大家一般这么写:
if((now-last)<time){
return;
}
last = now;
//do something
很多人已经用过了,只是不知道名称。
而特殊一点的节流需求:
时间段内,只执行最后一次触发,丢掉之前的触发。
碰到的应用场景是,消息队列在时间段内有数据变化,在最后一次进行批量处理传递。
函数防抖
,我看到的使用场景:
按照套路,该亮出自己的代码来实现了。
然而 iOS 也早有人实现了轮子,不重复造轮子嘛,可以直接使用。
发现 MessageThrottle 是比较完备的实现,而且在手 Q 中应用了,质量比较可靠。推荐一下。
它的使用很简单:
Stub *s = [Stub new];
MTRule *rule = [MTRule new];
rule.target = s; // You can also assign `Stub.class` or `mt_metaClass(Stub.class)`
rule.selector = @selector(foo:);
rule.durationThreshold = 0.01;
[MTEngine.defaultEngine applyRule:rule]; // or use `[rule apply]`
主要就是对 MTRule
的设置,决定我们将以哪种模式,多少的时间限制来控制方法调用。
虽说不再造轮子,但要了解它是什么样的。当然如不感兴趣,看使用也够了。
整个库就只有 MessageThrottle.h
和 MessageThrottle.m
两个文件。
主要思路是:对进行节流和防抖的方法,进行 hook,然后再统一做处理。
其实里面能学习的点不少,这里只大概介绍一下。
引用作者自己说明主要类关系的图,虚线代表弱引用:
MTEngine 中通过 NSMapTable
来以target
作为key,selector
数组作为 value,来存储管理数据。
NSMapTable
的一个特性是支持任意指针作为 Key 且无需持有,NSMapTable
也会自动移除那些键或值为 nil
的数据。
一个关键设计点在于,使用关联对象,将 MTDealloc
对象关联在 target
上:
- (MTDealloc *)mt_deallocObject
{
MTDealloc *mtDealloc = objc_getAssociatedObject(self.target, self.selector);
if (!mtDealloc) {
mtDealloc = [MTDealloc new];
mtDealloc.rule = self;
mtDealloc.cls = object_getClass(self.target);
objc_setAssociatedObject(self.target, self.selector, mtDealloc, OBJC_ASSOCIATION_RETAIN);
}
return mtDealloc;
}
关联对象设计的好处是:
在 target 释放时,关联对象也是会被清除的,所以 MTDealloc 对象也会释放,达到了 target 释放时自动移除 rule 的效果。
在 MTDealloc 的 dealloc 方法进行discard
操作:
- (void)dealloc
{
SEL selector = NSSelectorFromString(@"discardRule:whenTargetDealloc:");
((void (*)(id, SEL, MTRule *, MTDealloc *))[MTEngine.defaultEngine methodForSelector:selector])(MTEngine.defaultEngine, selector, self.rule, self);
}
里面调用写的有点骚...其实就是:
[MTEngine.defaultEngine discardRule:self.rule whenTargetDealloc:self];
整个库的核心处理在 mt_handleInvocation
中:
/**
处理执行 NSInvocation
@param invocation NSInvocation 对象
@param rule MTRule 对象
*/
static void mt_handleInvocation(NSInvocation *invocation, MTRule *rule)
{
NSCParameterAssert(invocation);
NSCParameterAssert(rule);
if (!rule.isActive) {//规则非 active 状态的,直接 invoke
[invocation invoke];
return;
}
if (rule.durationThreshold <= 0 || mt_invokeFilterBlock(rule, invocation)) {//时间小于等于0,设置aliasSelector(为原始方法IMP)后执行.
invocation.selector = rule.aliasSelector;
[invocation invoke];
return;
}
//时间戳处理,用 correctionForSystemTime 校正系统时间所需的差值。
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
now += MTEngine.defaultEngine.correctionForSystemTime;
switch (rule.mode) {
//节流模式:执行第一次触发
case MTPerformModeFirstly: {
//触发时,直接看现在的时间间隔是否比限制时间大,如果大于则直接执行,否则不响应
if (now - rule.lastTimeRequest > rule.durationThreshold) {
invocation.selector = rule.aliasSelector;
[invocation invoke];
//执行后,更新最近执行时间
rule.lastTimeRequest = now;
dispatch_async(rule.messageQueue, ^{
// May switch from other modes, set nil just in case.
rule.lastInvocation = nil;
});
}
break;
}
//节流模式:执行最后一次触发
case MTPerformModeLast: {
invocation.selector = rule.aliasSelector;
//invocation 提前持有参数,防止延迟执行时被释放掉
[invocation retainArguments];
dispatch_async(rule.messageQueue, ^{
//更新最近触发的 invocation
rule.lastInvocation = invocation;
//如间隔时间超出 rule 限定时间,则对方法做执行。保证为最后一次调用
if (now - rule.lastTimeRequest > rule.durationThreshold) {
//更新执行时间
rule.lastTimeRequest = now;
//按规则的间隔时间后执行 invoke
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
if (!rule.isActive) {
rule.lastInvocation.selector = rule.selector;
}
[rule.lastInvocation invoke];
//invoke 后将 lastInvocation 置 nil
rule.lastInvocation = nil;
});
}
});
break;
}
//防抖模式:一段时间内不再有新触发,再执行
case MTPerformModeDebounce: {
//设置 invocation 的 selector
invocation.selector = rule.aliasSelector;
//提前持有参数
[invocation retainArguments];
dispatch_async(rule.messageQueue, ^{
//更新 invocation
rule.lastInvocation = invocation;
//在限制时间段过后做执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
//假如还是rule.invocation 和 invocation一样,证明没有新的触发,达到执行条件
if (rule.lastInvocation == invocation) {
if (!rule.isActive) {
rule.lastInvocation.selector = rule.selector;
}
[rule.lastInvocation invoke];
rule.lastInvocation = nil;
}
});
});
break;
}
}
}
阅读量:528
点赞量:0
收藏量:0