函数防抖与节流
今天来和大家谈论一个非常有意思的话题,就是函数节流和函数防抖。
可能大家还不是非常了解这两个术语的意思,让我们先来看下他们的含义吧。
Throttling enforces a maximum number of times a function can be called over time. As in “execute this function at most once every 100 milliseconds.”
首先是函数节流(Throttling),意思就是说一个函数在一定时间内,只能执行有限次数。比如,一个函数在100毫秒呢,最多只能执行一次。当然,也可以不执行!
看完了节流,我们再来看看函数防抖(Debouncing)。
Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called. As in “execute this function only if 100 milliseconds have passed without it being called.”
函数防抖的意思就是,一个函数被执行过一次以后,在一段时间内不能再次执行。比如,一个函数执行完了之后,100毫秒之内不能第二次执行。
咦?有人看到这,肯定就有疑惑了,这两玩意有啥区别啊?这么解释吧,函数防抖可能会被无限延迟。用现实乘坐公交车中的例子来说,Throttle就是准点就发车(比如15分钟一班公交车);Debounce就是黑车,上了一个人以后,司机说,再等一个人,等不到,咱么10分钟后出发。但是呢,如果在10分钟内又有一个人上车,这个10分钟自动延后直到等待的10分钟内没人上车了。换句话说,Debounce可以理解成merge一段时间的一系列相同函数调用。
如果还不能理解,这里有个很好玩的在线演示 (PS:你必须用电脑上)
JavaScript中的函数节流和防抖
说到防抖和节流,我们就不得不先来提一提JavaScript中坑爹的DOM操作。我们直接看underscore.js中的源码:
首先是Throttle:
_.throttle = function(func, wait) {
var context, args, result;
var timeout = null;
var previous = 0;
var later = function() {
// 若设定了开始边界不执行选项,上次执行时间始终为0
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
var now = _.now();
// 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。
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) {
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;
};
};
代码逻辑非常好理解,
var remaining = wait - (now - previous);
就是计算下上一次调用的时间和现在所处的时间间隔是不是超过了wait的时间(这个时间只有可能会大于等于wait,因为runloop是很繁忙的,前面一个任务很耗时,那你就多等一会呗)。
可能会有人不理解option是啥意思,我们看下代码中涉及了以下两个参数:
options.leading和options.trailing
这两个参数的意思就是禁止第一次调用和最后一次调用,简单吧。
理解了函数节流,我们再来看看防抖是怎么做的。
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
// 据上一次触发时间间隔
var last = _.now() - timestamp;
// 上次被包装函数被调用时间间隔last小于设定时间间隔wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate && !timeout;
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
可以看到,在防抖的函数中包含了一个timestamp,这个参数用来记录了当前这个函数最后一次被调用是什么时候,每次调用的时候,就更新了timestamp。有人可能会问,这个timestamp在多次调用的过程中还能保留吗?答案是肯定的,debounce函数返回的实际是一个匿名函数,这个匿名函数就是一个闭包环境,可以捕捉timestap进行持久话访问,所以多次调用这个匿名函数实际上访问的都是同一个timestamp。
实现iOS中的函数节流
重头戏来啦!!!!。
在iOS中,我们经常会遇到这样的需求,比如说你要根据UIScrollView的contentOffset进行某些计算。这些计算有的很简单不耗时,比如根据offset动态的更改UINavigationBar的alpha值。但是有些就很复杂,比如你要根据某些offset启动某些动画,或者进行大规模的运算,还有很多时候时候会发送很多异步的API请求,由于很多用户会不停的用指尖在完美的iPhone屏幕上来回滑动,你一定不想你的App甚至是服务器这些操作给玩崩了,所以,函数节流是必不可少的。
初级版本
第一个版本的函数节流如下:
#import "PerformSelectorWithDebounce.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation NSObject (Throttle)
const char * WZThrottledSelectorKey;
- (void) wz_performSelector:(SEL)aSelector withThrottle:(NSTimeInterval)duration
{
NSMutableDictionary *blockedSelectors = objc_getAssociatedObject(self, WZThrottledSelectorKey);
if(!blockedSelectors)
{
blockedSelectors = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, kDictionaryOfSelectorsToBlock, blockedSelectors, OBJC_ASSOCIATION_RETAIN);
}
NSString * selectorName = NSStringFromSelector(aSelector);
if(![blockedSelectors objectForKey:aSelectorAsStr])
{
[blockedSelectors setObject:selectorName selectorName];
objc_msgSend(self, aSelector);
[self performSelector:@selector(unlockSelector:) withObject:aSelectorAsStr afterDelay:duration];
}
}
-(void)unlockSelector:(NSString*)selectorAsString
{
NSMutableDictionary *blockedSelectors = objc_getAssociatedObject(self, WZThrottledSelectorKey);
[blockedSelectors removeObjectForKey:selectorAsString];
}
初看这个代码逻辑,这个代码基本实现了在一定时间内只调用有限次数的函数。但是,这哪是函数节流啊,这是函数断流!不信你们在[UIScrollView scrollViewDidScroll]试试看,你们就知道啥叫断流了。原因也很简单,我们的unlockSelector是基于performSelector解锁的,而performSelector是基于runloop的,我们在不停的滚动的时候就会导致整个主runloop被我们占用,因此,unlock的函数一直没有得到调用,结果就导致了断流。
高级版本
因此,我又实现了一个高级版本,代码如下,需要测试用例和源码的可以上我的github自取,地址戳我。代码逻辑很简单同时也解决了上述版本所提及的问题。
#import "NSObject+Throttle.h"
#import <objc/runtime.h>
#import <objc/message.h>
static char WZThrottledSelectorKey;
static char WZThrottledSerialQueue;
@implementation NSObject (Throttle)
- (void)wz_performSelector:(SEL)aSelector withThrottle:(NSTimeInterval)inteval
{
dispatch_async([self getSerialQueue], ^{
NSMutableDictionary *blockedSelectors = [objc_getAssociatedObject(self, &WZThrottledSelectorKey) mutableCopy];
if (!blockedSelectors) {
blockedSelectors = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &WZThrottledSelectorKey, blockedSelectors, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
NSString *selectorName = NSStringFromSelector(aSelector);
if (![blockedSelectors objectForKey:selectorName]) {
[blockedSelectors setObject:selectorName forKey:selectorName];
objc_setAssociatedObject(self, &WZThrottledSelectorKey, blockedSelectors, OBJC_ASSOCIATION_COPY_NONATOMIC);
dispatch_async(dispatch_get_main_queue(), ^{
objc_msgSend(self, aSelector);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(inteval * NSEC_PER_SEC)), [self getSerialQueue], ^{
[self unlockSelector:selectorName];
});
});
}
});
}
#pragma mark - Private
- (void)unlockSelector:(NSString *)selectorName
{
dispatch_async([self getSerialQueue], ^{
NSMutableDictionary *blockedSelectors = [objc_getAssociatedObject(self, &WZThrottledSelectorKey) mutableCopy];
if ([blockedSelectors objectForKey:selectorName]) {
[blockedSelectors removeObjectForKey:selectorName];
}
objc_setAssociatedObject(self, &WZThrottledSelectorKey, blockedSelectors, OBJC_ASSOCIATION_COPY_NONATOMIC);
});
}
- (dispatch_queue_t)getSerialQueue
{
dispatch_queue_t serialQueur = objc_getAssociatedObject(self, &WZThrottledSerialQueue);
if (!serialQueur) {
serialQueur = dispatch_queue_create("com.satanwoo.throttle", NULL);
objc_setAssociatedObject(self, &WZThrottledSerialQueue, serialQueur, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return serialQueur;
}
@end