fix debounce
This commit is contained in:
parent
ba2b7b0c4a
commit
cd5a7e451f
9 changed files with 103 additions and 54 deletions
|
@ -1,15 +1,7 @@
|
|||
function debounce(f, ms) {
|
||||
|
||||
let isCooldown = false;
|
||||
|
||||
function debounce(func, ms) {
|
||||
let timeout;
|
||||
return function() {
|
||||
if (isCooldown) return;
|
||||
|
||||
f.apply(this, arguments);
|
||||
|
||||
isCooldown = true;
|
||||
|
||||
setTimeout(() => isCooldown = false, ms);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, arguments), ms);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,33 +7,30 @@ describe("debounce", function() {
|
|||
this.clock.restore();
|
||||
});
|
||||
|
||||
it("trigger the fuction execution immediately", function () {
|
||||
let mode;
|
||||
const f = () => mode='leading';
|
||||
|
||||
debounce(f, 1000)(); // runs without a delay
|
||||
it("for one call - runs it after given ms", function () {
|
||||
const f = sinon.spy();
|
||||
const debounced = debounce(f, 1000);
|
||||
|
||||
assert.equal(mode, 'leading');
|
||||
debounced("test");
|
||||
assert(f.notCalled);
|
||||
this.clock.tick(1000);
|
||||
assert(f.calledOnceWith("test"));
|
||||
});
|
||||
|
||||
it("calls the function at maximum once in ms milliseconds", function() {
|
||||
let log = '';
|
||||
it("for 3 calls - runs the last one after given ms", function() {
|
||||
const f = sinon.spy();
|
||||
const debounced = debounce(f, 1000);
|
||||
|
||||
function f(a) {
|
||||
log += a;
|
||||
}
|
||||
f("a")
|
||||
setTimeout(() => f("b"), 200); // ignored (too early)
|
||||
setTimeout(() => f("c"), 500); // runs (1000 ms passed)
|
||||
this.clock.tick(1000);
|
||||
|
||||
f = debounce(f, 1000);
|
||||
assert(f.notCalled);
|
||||
|
||||
f(1); // runs at once
|
||||
f(2); // ignored
|
||||
this.clock.tick(500);
|
||||
|
||||
setTimeout(() => f(3), 100); // ignored (too early)
|
||||
setTimeout(() => f(4), 1100); // runs (1000 ms passed)
|
||||
setTimeout(() => f(5), 1500); // ignored (less than 1000 ms from the last run)
|
||||
|
||||
this.clock.tick(5000);
|
||||
assert.equal(log, "14");
|
||||
assert(f.calledOnceWith('c'));
|
||||
});
|
||||
|
||||
it("keeps the context of the call", function() {
|
||||
|
@ -45,6 +42,7 @@ describe("debounce", function() {
|
|||
|
||||
obj.f = debounce(obj.f, 1000);
|
||||
obj.f("test");
|
||||
this.clock.tick(5000);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="183" viewBox="0 0 500 183"><defs><style>@import url(https://fonts.googleapis.com/css?family=Open+Sans:bold,italic,bolditalic%7CPT+Mono);@font-face{font-family:'PT Mono';font-weight:700;font-style:normal;src:local('PT MonoBold'),url(/font/PTMonoBold.woff2) format('woff2'),url(/font/PTMonoBold.woff) format('woff'),url(/font/PTMonoBold.ttf) format('truetype')}</style></defs><g id="combined" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="debounce.svg"><text id="200" fill="#8A704D" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="115" y="138">200</tspan></text><text id="1500" fill="#8A704D" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="314" y="141">1500</tspan></text><path id="Shape" fill="#EE6B47" stroke="#EE6B47" stroke-linecap="square" stroke-width="3" d="M333.378 27.32L340.92 11 321 32.024l9.674 2.932L324.797 51 343 30.253l-9.622-2.932zm4.369-11.617l-5.669 12.226 9.206 2.767-13.887 15.989 4.525-12.338-9.206-2.766 15.031-15.878z"/><text id="0" fill="#8A704D" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="83" y="138">0</tspan></text><text id="c" fill="#EE6B47" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="323" y="74">c</tspan></text><text id="f(a)" fill="#EE6B47" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="71" y="164">f(a)</tspan></text><text id="f(b)" fill="#EE6B47" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="111" y="164">f(b)</tspan></text><text id="f(c)" fill="#EE6B47" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="171" y="164">f(c)</tspan></text><path id="Line-Copy" stroke="#BCA68E" stroke-linecap="square" stroke-width="2" d="M127.5 120V85"/><text id="500" fill="#8A704D" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="175" y="138">500</tspan></text><path id="Line-Copy" stroke="#BCA68E" stroke-linecap="square" stroke-width="2" d="M87.5 120V85"/><text id="time" fill="#8A704D" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="417" y="125">time</tspan></text><path id="Line" fill="#BCA68E" fill-rule="nonzero" d="M422.369 94.388l.871.49 12 6.75 1.55.872-1.55.872-12 6.75-.871.49-.98-1.743.87-.49 8.673-4.879H38v-2h392.932l-8.672-4.878-.872-.49.98-1.744z"/><path id="Line-Copy" stroke="#BCA68E" stroke-linecap="square" stroke-width="2" d="M187.5 120V85"/><path id="Line-2" stroke="#EE6B47" stroke-linecap="square" stroke-width="3" d="M327.5 120V85"/><text id="calls:" fill="#8A704D" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="7" y="164">calls:</tspan></text><text id="after-1000ms" fill="#8A704D" font-family="PTMono-Regular, PT Mono" font-size="14" font-weight="normal"><tspan x="348" y="35"> after 1000ms</tspan></text></g></g></svg>
|
After Width: | Height: | Size: 3 KiB |
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
|
||||
|
||||
Function <code>handler</code> is called on this input:
|
||||
<br>
|
||||
<input id="input1" placeholder="type here">
|
||||
|
||||
<p>
|
||||
|
||||
Debounced function <code>debounce(handler, 1000)</code> is called on this input:
|
||||
<br>
|
||||
<input id="input2" placeholder="type here">
|
||||
|
||||
<p>
|
||||
<button id="result">The <code>handler</code> puts the current result here</button>
|
||||
|
||||
<script>
|
||||
function handler(event) {
|
||||
result.innerHTML = event.target.value;
|
||||
}
|
||||
|
||||
input1.oninput = handler;
|
||||
input2.oninput = _.debounce(handler, 1000);
|
||||
</script>
|
|
@ -4,21 +4,50 @@ importance: 5
|
|||
|
||||
# Debounce decorator
|
||||
|
||||
The result of `debounce(f, ms)` decorator should be a wrapper that passes the call to `f` at maximum once per `ms` milliseconds.
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
|
||||
|
||||
In other words, when we call a "debounced" function, it guarantees that all future calls to the function made less than `ms` milliseconds after the previous call will be ignored.
|
||||
The result of `debounce(f, ms)` decorator should be a wrapper that suspends any calls to `f` and invokes `f` once after `ms` of inactivity.
|
||||
|
||||
For instance:
|
||||
Let's say we had a function `f` and replaced it with `f = debounce(f, 1000)`.
|
||||
|
||||
```js no-beautify
|
||||
let f = debounce(alert, 1000);
|
||||
Then if the wrapped function is called at 0ms, 200ms and 500ms, and then there are no calls, then the actual `f` will be only called once, at 1500ms. That is: after the cooldown period of 1000ms from the last call.
|
||||
|
||||
f(1); // runs immediately
|
||||
f(2); // ignored
|
||||

|
||||
|
||||
setTimeout( () => f(3), 100); // ignored ( only 100 ms passed )
|
||||
setTimeout( () => f(4), 1100); // runs
|
||||
setTimeout( () => f(5), 1500); // ignored (less than 1000 ms from the last run)
|
||||
...And it will get the arguments of the very last call, other calls are ignored.
|
||||
|
||||
Here's the code (uses the debounce decorator from the [Lodash library](https://lodash.com/docs/4.17.15#debounce):
|
||||
|
||||
```js
|
||||
let f = _.debounce(alert, 1000);
|
||||
|
||||
f("a");
|
||||
setTimeout( () => f("b"), 200);
|
||||
setTimeout( () => f("c"), 500);
|
||||
// debounced function waits 1000ms after the last call and then runs: alert("c")
|
||||
```
|
||||
|
||||
In practice `debounce` is useful for functions that retrieve/update something when we know that nothing new can be done in such a short period of time, so it's better not to waste resources.
|
||||
|
||||
Now a practical example. Let's say, the user types something, and we'd like to send a request to the server once they're finished.
|
||||
|
||||
There's no point in sending the request for every character typed. Instead we'd like to wait, and then process the whole result. The `debounce` decorator makes this easy.
|
||||
|
||||
In a web-browser, we can setup an event handler -- a function that's called on every change of an input field. Normally, an event handler is called very often, for every typed key. But if we `debounce` it by 1000ms, then it will be only called once, after 1000ms after the last input.
|
||||
|
||||
```online
|
||||
|
||||
In this live example, the handler puts the result into a box below, try it:
|
||||
|
||||
[iframe border=1 src="debounce" height=200]
|
||||
|
||||
See? The second input calls the debounced function, so its content is processed after 1000ms from the last input.
|
||||
```
|
||||
|
||||
So, `debounce` is a great way to process a sequence of events: be it a sequence of key presses, mouse movements or something else.
|
||||
|
||||
|
||||
It waits the given time after the last call, and then runs its function, that can process the result.
|
||||
|
||||
Implement `debounce` decorator.
|
||||
|
||||
Hint: that's just a few lines if you think about it :)
|
|
@ -4,16 +4,19 @@ importance: 5
|
|||
|
||||
# Throttle decorator
|
||||
|
||||
Create a "throttling" decorator `throttle(f, ms)` -- that returns a wrapper, passing the call to `f` at maximum once per `ms` milliseconds. Those calls that fall into the "cooldown" period, are ignored.
|
||||
Create a "throttling" decorator `throttle(f, ms)` -- that returns a wrapper.
|
||||
|
||||
**The difference with `debounce` -- if an ignored call is the last during the cooldown, then it executes at the end of the delay.**
|
||||
When it's called multiple times, it passes the call to `f` at maximum once per `ms` milliseconds.
|
||||
|
||||
The difference with debounce is that it's completely different decorator:
|
||||
- `debounce` runs the function once after the "cooldown" period. Good for processing the final result.
|
||||
- `throttle` runs it not more often than given `ms` time. Good for regular updates that shouldn't be very often.
|
||||
|
||||
Let's check the real-life application to better understand that requirement and to see where it comes from.
|
||||
|
||||
**For instance, we want to track mouse movements.**
|
||||
|
||||
In a browser we can setup a function to run at every mouse movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms).
|
||||
|
||||
**We'd like to update some information on the web-page when the pointer moves.**
|
||||
|
||||
...But updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in updating more often than once per 100ms.
|
||||
|
|
|
@ -209,7 +209,7 @@ To make it all clear, let's see more deeply how `this` is passed along:
|
|||
2. So when `worker.slow(2)` is executed, the wrapper gets `2` as an argument and `this=worker` (it's the object before dot).
|
||||
3. Inside the wrapper, assuming the result is not yet cached, `func.call(this, x)` passes the current `this` (`=worker`) and the current argument (`=2`) to the original method.
|
||||
|
||||
## Going multi-argument with "func.apply"
|
||||
## Going multi-argument
|
||||
|
||||
Now let's make `cachingDecorator` even more universal. Till now it was working only with single-argument functions.
|
||||
|
||||
|
@ -236,7 +236,7 @@ There are many solutions possible:
|
|||
|
||||
For many practical applications, the 3rd variant is good enough, so we'll stick to it.
|
||||
|
||||
Also we need to replace `func.call(this, x)` with `func.call(this, ...arguments)`, to pass all arguments to the wrapped function call, not just the first one.
|
||||
Also we need to pass not just `x`, but all arguments in `func.call`. Let's recall that in a `function()` we can get a pseudo-array of its arguments as `arguments`, so `func.call(this, x)` should be replaced with `func.call(this, ...arguments)`.
|
||||
|
||||
Here's a more powerful `cachingDecorator`:
|
||||
|
||||
|
@ -284,6 +284,8 @@ There are two changes:
|
|||
- In the line `(*)` it calls `hash` to create a single key from `arguments`. Here we use a simple "joining" function that turns arguments `(3, 5)` into the key `"3,5"`. More complex cases may require other hashing functions.
|
||||
- Then `(**)` uses `func.call(this, ...arguments)` to pass both the context and all arguments the wrapper got (not just the first one) to the original function.
|
||||
|
||||
## func.apply
|
||||
|
||||
Instead of `func.call(this, ...arguments)` we could use `func.apply(this, arguments)`.
|
||||
|
||||
The syntax of built-in method [func.apply](mdn:js/Function/apply) is:
|
||||
|
@ -303,14 +305,14 @@ func.call(context, ...args); // pass an array as list with spread syntax
|
|||
func.apply(context, args); // is same as using call
|
||||
```
|
||||
|
||||
There's only a minor difference:
|
||||
There's only a subtle difference:
|
||||
|
||||
- The spread syntax `...` allows to pass *iterable* `args` as the list to `call`.
|
||||
- The `apply` accepts only *array-like* `args`.
|
||||
|
||||
So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works.
|
||||
So, where we expect an iterable, `call` works, and where we expect an array-like, `apply` works.
|
||||
|
||||
And for objects that are both iterable and array-like, like a real array, we technically could use any of them, but `apply` will probably be faster, because most JavaScript engines internally optimize it better.
|
||||
And for objects that are both iterable and array-like, like a real array, we can use any of them, but `apply` will probably be faster, because most JavaScript engines internally optimize it better.
|
||||
|
||||
Passing all arguments along with the context to another function is called *call forwarding*.
|
||||
|
||||
|
@ -344,7 +346,7 @@ function hash(args) {
|
|||
}
|
||||
```
|
||||
|
||||
...Unfortunately, that won't work. Because we are calling `hash(arguments)` and `arguments` object is both iterable and array-like, but not a real array.
|
||||
...Unfortunately, that won't work. Because we are calling `hash(arguments)`, and `arguments` object is both iterable and array-like, but not a real array.
|
||||
|
||||
So calling `join` on it would fail, as we can see below:
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue