节流装饰器
重要性:5
创建一个“节流”装饰器 throttle(f, ms)
- 它返回一个包装器。
当它被多次调用时,它最多每 ms
毫秒将调用传递给 f
一次。
与防抖装饰器相比,行为完全不同
debounce
在“冷却”时间段后运行一次函数。适用于处理最终结果。throttle
不会比给定的ms
时间更频繁地运行它。适用于不应该太频繁的定期更新。
换句话说,throttle
就像一个秘书,接受电话,但不会比每 ms
毫秒一次更频繁地打扰老板(调用实际的 f
)。
让我们检查一下现实生活中的应用,以更好地理解这个需求以及它的来源。
例如,我们想跟踪鼠标移动。
在浏览器中,我们可以设置一个函数,在每次鼠标移动时运行,并获取指针移动时的位置。在鼠标活动使用期间,此函数通常运行非常频繁,可能每秒运行 100 次(每 10 毫秒一次)。**我们希望在指针移动时更新网页上的某些信息。**
…但是更新函数 update()
太重了,无法在每次微小移动时都执行。也没有必要比每 100 毫秒更新一次更频繁地更新。
因此,我们将它包装到装饰器中:使用 `throttle(update, 100)` 作为每次鼠标移动时运行的函数,而不是原始的 `update()`。装饰器将被频繁调用,但最多每 100 毫秒将调用转发到 `update()` 一次。
从视觉上看,它将是这样的
- 对于第一次鼠标移动,装饰后的变体立即将调用传递给 `update`。这很重要,用户会立即看到我们对他们移动的反应。
- 然后,随着鼠标继续移动,直到 `100 毫秒` 内什么都不会发生。装饰后的变体忽略调用。
- 在 `100 毫秒` 结束时,将使用最后一个坐标执行一次 `update`。
- 最后,鼠标停在某个位置。装饰后的变体将等待 `100 毫秒` 过期,然后使用最后一个坐标运行 `update`。因此,非常重要的一点是,最终的鼠标坐标将被处理。
代码示例
function f(a) {
console.log(a);
}
// f1000 passes calls to f at maximum once per 1000 ms
let f1000 = throttle(f, 1000);
f1000(1); // shows 1
f1000(2); // (throttling, 1000ms not out yet)
f1000(3); // (throttling, 1000ms not out yet)
// when 1000 ms time out...
// ...outputs 3, intermediate value 2 was ignored
附注:传递给 `f1000` 的参数和上下文 `this` 应该传递给原始的 `f`。
function throttle(func, ms) {
let isThrottled = false,
savedArgs,
savedThis;
function wrapper() {
if (isThrottled) { // (2)
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
func.apply(this, arguments); // (1)
setTimeout(function() {
isThrottled = false; // (3)
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
调用 `throttle(func, ms)` 将返回 `wrapper`。
- 在第一次调用期间,`wrapper` 只运行 `func` 并设置冷却状态 ( `isThrottled = true` )。
- 在这种状态下,所有调用都将保存在 `savedArgs/savedThis` 中。请注意,上下文和参数同样重要,都应该被记忆。我们需要同时使用它们来重现调用。
- 在 `ms` 毫秒过去后,`setTimeout` 将触发。冷却状态将被移除 ( `isThrottled = false` ),并且如果我们忽略了调用,`wrapper` 将使用最后一个记忆的参数和上下文执行。
第三步运行的不是 `func`,而是 `wrapper`,因为我们不仅需要执行 `func`,还需要再次进入冷却状态并设置超时以重置它。