最近編寫(xiě)vue前端項(xiàng)目時(shí),了解了新的專業(yè)名詞:函數(shù)防抖、函數(shù)節(jié)流濒旦;雖是第一次知道這個(gè)詞,但平常開(kāi)發(fā)中其實(shí)早已接觸使用過(guò)了再登;
防抖與節(jié)流是處理頻繁事件的技術(shù):用以限制事件頻繁地發(fā)生可能造成的問(wèn)題尔邓,如:
- 多次執(zhí)行同一事件地熄,造成事件疊加帶來(lái)不必要的影響(iOS典型的例子是點(diǎn)擊按鈕Push到其他vc時(shí)會(huì)連續(xù)跳轉(zhuǎn)多次)
- 頻繁渲染界面等帶來(lái)的性能問(wèn)題
- 頻繁調(diào)用接口請(qǐng)求數(shù)據(jù)帶來(lái)的流量壓力
節(jié)流(throttle)
節(jié)流是指高頻事件觸發(fā)对湃,但在n秒內(nèi)只會(huì)執(zhí)行一次,所以節(jié)流會(huì)稀釋函數(shù)的執(zhí)行頻率掠河;
應(yīng)用場(chǎng)景:
頻繁點(diǎn)擊按鈕只響應(yīng)一次事件
防抖(debounce)
防抖是指觸發(fā)高頻事件后n秒內(nèi)函數(shù)只會(huì)執(zhí)行一次沽损,如果n秒內(nèi)高頻事件再次被觸發(fā)灯节,則重新計(jì)算時(shí)間;
應(yīng)用場(chǎng)景:
輸入框輸入文字時(shí)實(shí)時(shí)搜索功能
節(jié)流和防抖的區(qū)別
簡(jiǎn)單來(lái)說(shuō)節(jié)流是控制頻率,防抖是控制次數(shù)
單從字面理解的話绵估,防抖節(jié)流容易搞混且不太好理解炎疆;下面引用一張圖說(shuō)明:
以上圖基礎(chǔ) 舉個(gè)例子:
Button點(diǎn)擊事件有可能存在極短時(shí)間內(nèi),頻繁的點(diǎn)擊了多次的情況壹士;針對(duì)這種情況分別做節(jié)流和防抖處理interval設(shè)置為1000ms磷雇,實(shí)際的效果如下:
- 函數(shù)節(jié)流
在1000ms內(nèi)連續(xù)點(diǎn)擊了3次,button響應(yīng)事件a躏救、x唯笙、y螟蒸;最終只會(huì)立即執(zhí)行a,丟棄x崩掘、y七嫌;如果在m ms內(nèi)連續(xù)點(diǎn)擊了n 次,最終會(huì)執(zhí)行時(shí)間 m/1000 + 1 個(gè)事件苞慢;即不管有多少事件interval計(jì)算是固定的诵原; - 函數(shù)防抖
在1000ms內(nèi)連續(xù)點(diǎn)擊了2次b、c挽放;最終會(huì)已最后一次點(diǎn)擊c為準(zhǔn)重新計(jì)時(shí)绍赛,當(dāng)時(shí)間達(dá)到interval才執(zhí)行c;而前面的b會(huì)丟棄辑畦;如果在m ms內(nèi)連續(xù)點(diǎn)擊了n 次吗蚌,且每每2個(gè)點(diǎn)擊間隔都沒(méi)有超過(guò)1000ms最終只會(huì)在n點(diǎn)擊1000ms后執(zhí)行第n個(gè)事件(只有一個(gè)事件);即interval計(jì)算是動(dòng)態(tài)的纯出,每次都會(huì)以最后一個(gè)重新計(jì)時(shí)蚯妇;
代碼實(shí)現(xiàn)
前端實(shí)現(xiàn)
//
export default {
// 防抖
debounce: function (fn, interval) {
// 時(shí)間間隔ms
var interval = interval || 200;
var timer;
// 閉包
return function () {
// 考慮作用域,上下文環(huán)境暂筝,apply需要用到this對(duì)象
var self = this;
// 接收的參數(shù)用 ES6 中的 rest 參數(shù)統(tǒng)一存儲(chǔ)到變量 args 中箩言。arguments就是傳入的參數(shù)數(shù)組,而且個(gè)數(shù)可以不確定的傳回給fn(不確定函數(shù)到底有多少個(gè)參數(shù),用arguments來(lái)接收)
var args = arguments;
// 判斷還在定時(shí)焕襟,說(shuō)明當(dāng)前正在一個(gè)計(jì)時(shí)過(guò)程中陨收,并且又觸發(fā)了相同事件。所以要取消當(dāng)前的計(jì)時(shí)鸵赖,重新開(kāi)始計(jì)時(shí)
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
timer = null;
// 執(zhí)行方法
fn.apply(self, args);
}, interval);
};
},
// 節(jié)流
throttle: function (func, interval) {
var timer = null
var startTime = 0
return function () {
// 結(jié)束時(shí)間
var curTime = Date.now()
// 間隔時(shí)間 = 延遲的時(shí)間 - (結(jié)束時(shí)間戳 - 開(kāi)始時(shí)間戳)
var seconds = curTime - startTime
var self = this
var args = arguments
clearTimeout(timer)
if (seconds > interval) {
// 證明可以觸發(fā)了
func.apply(self, args)
// 重新計(jì)算開(kāi)始時(shí)間
startTime = Date.now()
}
}
}
}
使用畏吓,以實(shí)時(shí)搜索為例:
// html
<van-search
v-model="searchValue"
placeholder="輸入姓名查詢"
@input="onInput"
/>
// js
methods: {
onInput: debounceThrottleTool.debounce(function() {
this.queryUsers()
}, 800)
}
iOS實(shí)現(xiàn)
知道了思路后,其實(shí)不同語(yǔ)言實(shí)現(xiàn)起來(lái)都不難了卫漫;iOS這邊可以直接使用dispatch實(shí)現(xiàn)(當(dāng)然也有其他方式)
@implementation MMThrottler
- (instancetype)init {
self = [super init];
if (self) {
_interval = .2;
_queue = dispatch_get_main_queue();
_previousDate = NSDate.distantPast;
_semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)execute:(void(^)())action {
__weak typeof(self) selfWeak = self;
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_blockItem) {
// 取消上一次事件
dispatch_block_cancel(_blockItem);
}
_blockItem = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
selfWeak.previousDate = NSDate.date;
action();
});
NSTimeInterval seconds = [NSDate.date timeIntervalSinceDate:_previousDate];
NSTimeInterval delaySeconds = seconds > _interval ? 0 : _interval;
if (seconds > _interval) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC)), _queue, _blockItem);
}
dispatch_semaphore_signal(_semaphore);
}
@end
@implementation MMDebouncer
- (instancetype)init {
self = [super init];
if (self) {
_interval = .5;
_queue = dispatch_get_main_queue();
_semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)execute:(void(^)())action {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_blockItem) {
// 取消上一次事件
dispatch_block_cancel(_blockItem);
}
_blockItem = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
action();
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_interval * NSEC_PER_SEC)), _queue, _blockItem);
dispatch_semaphore_signal(_semaphore);
}
@end
使用菲饼,以實(shí)時(shí)搜索為例
#pragma mark - UITextFieldDelegate -
- (void)searchTextDidChange {
WeakObj(self)
[self.debouncer execute:^{
[selfWeak queryUsers];
}];
}
響應(yīng)式框架
如果項(xiàng)目使用了響應(yīng)式框架,這些框架一般都封裝了函數(shù)節(jié)流列赎、函數(shù)防抖功能宏悦;
RxSwift可以很簡(jiǎn)單的使用節(jié)流和防抖:
textfield.rx.text.orEmpty.changed
.debounce(0.3, scheduler: MainScheduler.instance)
.asObservable()
.subscribe(onNext: { [weak self] response in
}).disposed(by: rx.disposeBag)
textfield.rx.text.orEmpty.changed
.throttle(0.3, scheduler: MainScheduler.instance)
.asObservable()
.subscribe(onNext: { [weak self] response in
}).disposed(by: rx.disposeBag)