防抖和节流
一、防抖(debounce)
指定时间内,方法只能执行一次,多余的事件直接忽略掉(underscore, lodash库的debounce都是这样做的)。而这个时间的计算,是从最后一次触发监听事件开始算起。
比如:
- 按关闭电梯门的按钮时,电梯并不会里面关门,而是等一等看看是否有人要上电梯(即操作要延迟执行);
resize
事件(如果只是看resize后最终的效果,可以利用debounce);- 提交按钮防抖操作(一般操作需要立马执行)。
1.2 基于概念实现:
实现debounce
的关键点是如何计算delay时间(实现throttle
的关键点也一样)。
function debounce(cb, delay) {
var timeoutId;
return function debounced() {
// 每次调用都清除上次的延迟,然后重新创建延迟
timeoutId&& clearTimeout(timeoutId);
timeoutId= setTimeout(() => {
cb.apply(this, arguments);
}, delay)
}
}
1.3 各种实现中增加一些额外功能:
- 前缘(或者“immediate”)
是先执行操作后等待,还是先等待后执行操作。
前缘debounce:
延迟debounce:
两者计算delay时间的方式是一样的,唯一的区别就是触发调用函数的时间点。
- 取消操作
比如在计算延迟过程中组件卸载等导致的了清除操作。
带有"取消操作"和“前缘”的防抖方法(非一次写出,多次优化的结果):
function debounce(cb, delay, immediate) {
var timeoutId;
// cancel操作,只依赖timeoutId,所以应避免cancel函数放在debounced函数里声明(这回导致每次调用debounced都会重新声明定义cancel)
var cancel = debounced.cancel = function cancel() {
if(timeoutId) {
clearTimeout(timeoutId);
timeoutId= null;
}
}
function debounced() {
var args = arguments;
var self = this;
function action() {
cb.apply(self, args);
}
// 利用timeoutId标记是否已经调用了cb
if(immediate && !timeoutId) {
action();
}
// 清除上一个delay
cancel();
/*
* 开启新的delay
* immediate=true表示delay取消操作,否则delay函数执行。
*/
timeoutId= setTimeout(immediate ? cancel : action, delay)
}
return debounced;
}
上面实现存在个问题,它是利用setTimeout
创建/取消延时的,而不是动态计算延时。这回导致如果连续的操作中存在耗时运算,会导致setTimeout
回调不能及时触发。
function sleep(delay) {
var pre = Date.now();
while(Date.now() - pre < delay) {}
}
var debounced = debounce((a, b) => {
console.log(`a+b=${a+b}`)
return a + b;
}, 100, true)
// Case1
debounced(1, 23)
sleep(delay) // 同步方式延迟,导致`debounced`里的`setTimeout(immediate ? cancel : action, delay)`回调函数不会被执行
debounced(1, 24) // 虽然时间过去600ms了,但是还是被忽略掉了。
// Case2
debounced(1, 25)
setTimeout(() => {
debounced(1, 26) // 改成异步,就可以触发了
}, 600)
不过这样的实现相对简单清晰,并且绝大部分情况不会存在问题。
underscore debounce也是采用类似方式。
二、节流(throttle)
指定时间内(执行时间窗口),方法只能执行一次,多余的事件直接忽略掉(underscore, lodash库的throttle都是这样做的)。而这个时间的计算是从上次执行方法开始算起。
比如:上划展示更多商品时调用接口的频率会降低,一般处理scroll
, resize
, touchmove
, mousemove
等事件的处理函数。
2.1 基于概念的实现
function throttle(func, delay) {
var timeoutId;
var cancel = throttled.cancel = function cancel() {
if(timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
}
function throttled() {
var context = this;
var args = arguments;
if(!timeoutId) {
timeoutId = setTimeout(function() {
cancel();
action();
}, delay)
}
function action() {
func.apply(context, args);
}
}
return throttled;
}
- 逻辑简单,但
func
只能延迟执行; - 严格来说时间计算规则是不对的,这个实现的时间计算是从首次
throttled
函数调用计算的,而正确的应该是从上一次func
调用开始计算。
2.2 改进:并借鉴增加前缘(throttle里称为leading,trailing调用)控制
leading方式调用时,delay的时间得是动态的,因为在一个时间段内,后调用的throttled函数被delay时间就得短。
function throttle(func, delay, leading) {
var timeoutId, preCallTime;
var cancel = throttled.cancel = function cancel() {
if(timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
}
function throttled() {
var context = this;
var args = arguments;
var now = Date.now();
// 头部方式调用
if(leading) {
if(!preCallTime) {
preCallTime = 0;
}
var remain = delay - (now - preCallTime);
// 利用剩余时间控制是否可以调用action(可防止多次触发action)
if(remain <= 0) {
action();
}
preCallTime = now;
return;
}
if(!timeoutId) {
timeoutId = setTimeout(function() {
cancel();
action();
}, delay)
}
function action() {
func.apply(context, args);
}
}
return throttled;
}
- leading方式通过计算剩余时间控制执行
func
,已经不依赖setTimeout
了; - trailing方式回调函数必须延迟执行,依赖
setTimeout
不能避免; - trailing方式代码逻辑中
delay
本质也是剩余时间,还可以优化下代码组织。
2.3 再改进
function throttle(func, delay, leading) {
var timeoutId, preCallTime;
var cancel = throttled.cancel = function cancel() {
if(timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
}
function throttled() {
var context = this;
var args = arguments;
var now = Date.now();
if(!preCallTime) {
preCallTime = leading ? 0 : now;
}
var remain = delay - (now - preCallTime);
if(remain <= 0) {
action();
// 未达到规定时间 & trailing方式时的首次调用
} else if(!leading && !timeoutId) {
timeoutId = setTimeout(action, remain);
}
function action() {
func.apply(context, args);
preCallTime = now;
cancel();
}
}
return throttled;
}
总结
从上面的描述我们可以理解到防抖和节流都是控制“指定时间内,方法只能执行一次”,它们的区别在于如何计算时间间隔:
debounce
从最后一次调用debounced
函数开始算起(新触发的会覆盖上一次触发的);
防抖动
throttle
是从上次调用throttled
方法开始算起(新出发的时间 - 上次触发的时间)。
节流,控制频率
实现合一?
lodash确实合一了,但是代码可读性很差。
整理自gitHub笔记
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。