实现一个 debounce 函数
先来一段 underscore 里面的函数注释
When a sequence of calls of the returned function ends, the argument
function is triggered. The end of a sequence is defined by thewait
parameter. Ifimmediate
is passed, the argument function will be
triggered at the beginning of the sequence instead of at the end.
可以知道,在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。debounce 是一个高阶函数,返回的函数是作为某个事件回调来使用的。在给定的超时时间之内如果重复调用会重新计时。
使用场合:
- 输入框监听最后的提交事件
- resize, mousemove, scroll 等最后的刷新重绘
需要合并多次事件延迟触发成一次的其他场合,而不需要跟踪中间状态
生活中的实例: 如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。
初始版本
根据上面的描述,可以知道每次调用的时候,需要重新开始计时,于是得到第一个初始版本。
function debounce(func, wait) {
let timeout;
let debounced = () => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
func();
timeout = null;
}, wait);
}
return debounced;
}
由于返回的 debounced 函数是有执行上下文的,例如下面的代码:
var debounce_event_func = debounce(myMouseOver, 1000, true);
document.querySelector("#app").addEventListener("mousemove", throttle_event_func);
添加执行上下文
throttle_event_func不仅仅需要接受 event 函数,还需要将 this 指向 Dom 元素,这里是 app 元素,所以要在 debounced 开始加上保存上下文的参数即可。
function debounce(func, wait) {
let timeout;
let debounced = () => {
let context = this; // 保存 Dom 引用
let args = arguments; // 保存参数 [event, ...]
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args); // 这里使用 apply
timeout = null;
}, wait);
}
return debounced;
}
立即执行
在 underscore 实现中,还有 immediate 这个参数,我们按照其语义修改函数定义
function debounce(func, wait, immediate) {
let timeout;
let debounced = () => {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
// 提取出公共部分
let lazy = () => {
result = func.apply(context, args);
timeout = null;
};
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(lazy, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(lazy, wait);
}
}
return debounced;
}
最后优化
最后,我们将函数执行结果返回(虽然可能没有用,但是应该返回一个值),添加 cancel 函数来取消操作。
从而我们得到了最终的 debounced 函数。
function debounce(func, wait, immediate) {
let timeout, result;
let debounced = () => {
let context = this;
let args = arguments;
let lazy = () => {
result = func.apply(context, args);
timeout = null;
};
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果 timeout != null, 表示是中途取消状态
let callNow = !timeout;
timeout = setTimeout(lazy, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(lazy, wait);
}
return result;
}
debounced.cancel = () => {
clearTimeout(timeout);
timeout = null;
}
return debounced;
}
window.debounce = debounce;
实现一个 throttle 函数
先来一段 underscore 里面的函数注释
Returns a function, that, when invoked, will only be triggered at most once
during a given window of time. Normally, the throttled function will run
as much as it can, without ever going more than once perwait
duration;
but if you'd like to disable the execution on the leading edge, pass
{leading: false}
. To disable execution on the trailing edge, ditto.
看起来和上面的 debounce 有点类似,也是限制高频操作的次数,但是并不是将多个重叠时间窗口的操作合并成一个,而是规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
使用场合
- resize, scroll, mousemove 等的动画
- 元素拖拽动画
需要以低频来跟踪状态的场合,需要跟踪中间状态。也就是给大量事件按时间做平均分配触发。
生活中的实例: 我们知道目前的一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。
初始版本
首先按照定义,可以使用时间戳来完成的版本。当函数触发,就检查当前时间戳减去保存的时间戳是否超出窗口。
function throttle(func, wait) {
let old = 0;
let throttled = () => {
let context = this;
let args = arguments;
let now = new Date().valueOf();
if (now - old > wait) {
func.apply(context, args);
old = now;
}
}
return throttled;
}
窗口尾部的事件触发
上面的实现可以完成 leading: true 同时 trailling: false 的功能,但是如果要想实现 trailling: true,就要使用定时器
function throttle(func, wait) {
let timeout;
let throttled = () => {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
func.apply(context, args);
timeout = null;
}, wait);
}
}
return throttled;
}
将时间戳和定时器的方案结合
上面的实现是 leading: false 同时 trailling: true 的功能,只需要将这两种方法一起使用,同时要保证时间戳触发之后,取消定时器。亦或定时器触发之后,取消时间戳即可。然后在函数参数上添加上 options 参数。
function throttle(func, wait, options) {
let old = 0;
let timeout, result;
if (!options) { options = {}; }
let throttled = () => {
let context = this;
let args = arguments;
let now = new Date().valueOf();
if (options.leading === false) {
old = now;
}
if (now - old > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context, args);
old = now;
//////
if (!timeout) context = args = null;
} else if (!timeout && options.trailling === true) {
timeout = setTimeout(function () {
old = new Date().valueOf();
func.apply(context, args);
timeout = null;
}, wait);
}
}
throttled.cancel = () => {
clearTimeout(timeout);
timeout = null;
context = args = null;
};
return throttled;
}
最终版本
添加 cancel 功能后,得到最后的实现。
function throttle(func, wait, options) {
let old = 0;
let timeout, result;
if (!options) { options = {}; }
let throttled = () => {
let context = this;
let args = arguments;
let now = new Date().valueOf();
if (options.leading === false) {
old = now;
}
if (now - old > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context, args);
old = now;
//////
if (!timeout) context = args = null;
} else if (!timeout && options.trailling === true) {
timeout = setTimeout(function () {
old = new Date().valueOf();
func.apply(context, args);
timeout = null;
}, wait);
}
}
throttled.cancel = () => {
clearTimeout(timeout);
timeout = null;
context = args = null;
};
return throttled;
}