This is the second day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021
The article Outlines
- Based on using
debounce
Using the demonstrationlodash.debounce
- Common problem: Passing different parameters
- Solution: Memoize
- Solution: With cache
- Common problems: Correct use of React components
debounce
- implementation
lodash.debounce
- Basic implementation
- Performance optimization
- implementation
options
- Just to be brief
throttle
Based on using
When elevator equipment detects someone entering the elevator, it resets a fixed time before closing the door.
Debounce and elevator waiting have similar logic.
Debounce returns a debounce function which, when called, will re-delay the execution of func for the specified time.
Debounce is an effective way to avoid consecutive triggers in a short period of time and executes logic after the trigger has finished. It is often used with events such as Element Resize and input keyDown.
debounce
Using the demonstration
Here’s a simple example: Get the size of an element after it changes size, and then proceed
<div class="box"></div>
Copy the code
.box {
width: 100px;
height: 100px;
background-color: green;
resize: both;
overflow: auto;
}
.box::after {
content: attr(data-size);
}
Copy the code
function getElementSizeInfo(target) {
const { width, height } = target.getBoundingClientRect();
const { offsetWidth, offsetHeight } = target;
return {
width,
height,
offsetWidth,
offsetHeight,
};
}
const resizeObserver = new ResizeObserver((entries) = > {
// resizeObserverCallback
for (const entry of entries) {
const sizeInfo = getElementSizeInfo(entry.target);
entry.target.dataset.size = sizeInfo.offsetWidth;
// ...}}); resizeObserver.observe(document.querySelector(".box"));
Copy the code
In this example, ResizeObserver is used to listen for element resizing, and the resizeObserverCallback logic is triggered multiple times when resizing is dragged.
The example here is simple and does not result in a frame jam. However, if the DOM tree is heavy, frequent getBoundingClientRect, offsetWidth, offsetHeight will lead to multiple redraws in a short period of time, increasing the performance burden. If the JS calculation logic following the size is very time-consuming, the Script will occupy the main thread for a long time, and multiple calls in a short time will cause the drag operation to be disconnected.
To optimize the experience, it is tempting to leave the time-wasting logic to the end of a continuous operation.
So how do you know if a continuous operation has ended? There seemed to be no Web API available, so debounce was the only way to simulate it.
It is estimated that the interval for continuous operation is n seconds. After an operation, wait n seconds. If a new operation is observed, the operation is continuous.
Easily rewrite code with Debounce
function getElementSizeInfo(target) {
const { width, height } = target.getBoundingClientRect();
const { offsetWidth, offsetHeight } = target;
return {
width,
height,
offsetWidth,
offsetHeight,
};
}
function resizeCallback(target) {
const sizeInfo = getElementSizeInfo(target);
target.dataset.size = sizeInfo.offsetWidth;
// ...
}
// Set the observation time to 800 ms
const debouncedResizeCallback = _.debounce(resizeCallback, 800);
const resizeObserver = new ResizeObserver((entries) = > {
for (const entry ofentries) { debouncedResizeCallback(entry.target); }}); resizeObserver.observe(document.querySelector(".box"));
Copy the code
Codepen. IO/curly210102…
lodash.debounce
In our daily development, we often use the debounce method provided by loDash, the JavaScript utility library. Take a look at what Lodash. Debounce comes with.
_.debounce(
func,
[(wait = 0)],
[
(options = {
leading: false.trailing: true.maxWait: 0,})]);Copy the code
In addition to the base parameters, Lodash. Debounce also provides an options configuration
leading
: Whether to call at the beginning of a continuous operationtrailing
: Whether to call at the end of a continuous operationmaxWait
: Maximum duration of a continuous operation
The default, of course, is the ending call (leading: false; The trailing: true)
Set leading: true if you need to give feedback immediately at the start of the operation
leading
和 trailing
At the same timetrue
Only multiple action methods are called at the end of the wait
In addition to the configuration, Lodash. Debounce also provides two manual manipulation methods for debounce function:
debounced.cancel()
: Cancels the tail call of this continuous operationdebounced.flush()
: Perform the continuous operation immediately
Common problem: Passing different parameters
In most scenarios, the Debmentioning function will handle the repeat operation of a single object.
So can Debounce handle multiple objects?
Going back to the ResizeObserver example, where we listened for a single element, we now add multiple elements.
<div class="container">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
Copy the code
.container {
display: flex;
overflow: auto;
resize: both;
width: 800px;
border: 1px solid # 333;
}
.box {
width: 100px;
height: 100px;
background-color: green;
margin: 10px;
flex: 1;
}
.box::after {
content: attr(data-size);
}
Copy the code
// omit duplicate code...
const debouncedResizeCallback = _.debounce(resizeCallback, 800);
const resizeObserver = new ResizeObserver((entries) = > {
for (const entry ofentries) { debouncedResizeCallback(entry.target); }});document.querySelectorAll(".box").forEach((el) = > {
resizeObserver.observe(el);
});
Copy the code
Codepen. IO/curly210102…
For clarity, there is a flexbox container wrapped around the element to resize multiple elements at the same time.
Resize the container so that all elements have changed but only the last element shows the changed width.
It’s not complicated to explain. Multiple objects correspond to a single debouncedResizeCallback. DebouncedResizeCallback is called only once, and the last object to call it happens to be the last.
Solution: Memoize
It’s easy to solve, just give each element a debouncedResizeCallback.
const memories = new WeakMap(a);const debouncedResizeCallback = (obj) = > {
if(! memories.has(obj)) { memories.set(obj, _.debounce(resizeCallback,800));
}
memories.get(obj)(obj);
};
Copy the code
Simplified writing using lodash implementation
const debouncedResizeCallback = _.wrap(
_.memoize(() = > _.debounce(resizeCallback)),
(getMemoizedFunc, obj) = > getMemoizedFunc(obj)(obj)
);
Copy the code
Wrap Creates a wrapper function with the wrapper function body as the second argument. Substitute the first argument as the first argument to the wrapper function, which is to bring _.memoize(…) The section is brought into the function as getMemoizedFunc, equivalent to
const debouncedResizeCallback = (obj) = >
_.memoize(() = > _.debounce(resizeCallback))(obj)(obj);
Copy the code
Memoize will return a cache creation/reading function, and _.memoize(() => _.debounce(resizeCallback))(obj) reading will return the debounce function corresponding to OBj.
Codepen. IO/curly210102…
This example on StackOverflow is another application scenario for the Memoize solution.
// Avoid frequent updates to the same id
function save(obj) {
console.log("saving", obj.name);
// syncToServer(obj);
}
const saveDebounced = _.wrap(
_.memoize(() = > _.debounce(save), _.property("id")),
(getMemoizedFunc, obj) = > getMemoizedFunc(obj)(obj)
);
saveDebounced({ id: 1.name: "Jim" });
saveDebounced({ id: 2.name: "Jane" });
saveDebounced({ id: 1.name: "James" });
/ / - saving James
/ / - saving Jane
Copy the code
To sum up, the memoize method is essentially assigning debmentioning function to each object.
Solution: With cache
The memoize method will produce a separate debwriting function, which will also have some drawbacks. If the Debounce package is a heavy operation, a single call involves a lot of recalculation and rerendering. Being able to gather calls to multiple objects into a single calculation and rendering can help optimize performance.
A collection is a cache space that records the called object.
function getDebounceWithCache(callback, ... debounceParams) {
const cache = new Set(a);const debounced = _.debounce(function () {
callback(cache);
cache.clear();
}, ...debounceParams);
return (items) = > {
items.forEach((item) = > cache.add(item));
debounced(cache);
};
}
const debouncedResizeCallback = getDebounceWithCache(resizeCallback, 800);
function resizeCallback(targets) {
targets.forEach((target) = > {
const sizeInfo = getElementSizeInfo(target);
target.dataset.size = sizeInfo.offsetWidth;
});
// ...
}
const resizeObserver = new ResizeObserver((entries) = > {
debouncedResizeCallback(entries.map((entry) = > entry.target));
});
document.querySelectorAll(".box").forEach((el) = > {
resizeObserver.observe(el);
});
Copy the code
Codepen. IO/curly210102…
In summary, the with Cache method essentially collects the called object as a whole and applies the whole to the final operation.
Common problem: Correct use of debounce in React components
Defining the debmentioning function in the React component requires attention to uniqueness.
Class component has no pit, can be defined as instance property
import debounce from "lodash.debounce";
import React from "react";
export default class Component extends React.Component {
state = {
value: ""};constructor(props) {
super(props);
this.debouncedOnUpdate = debounce(this.onUpdate, 800);
}
componentWillUnmount() {
this.debouncedOnUpdate.cancel();
}
onUpdate = (e) = > {
const target = e.target;
const value = target.value;
this.setState({
value,
});
};
render() {
return (
<div className="App">
<input onKeyDown={this.debouncedOnUpdate} />
<p>{this.state.value}</p>
</div>); }}Copy the code
Function components need to avoid re-declarations in re-render
import debounce from "lodash.debounce";
import React from "react";
export default function Component() {
const [value, setValue] = React.useState("");
const debouncedOnUpdateRef = React.useRef(null);
React.useEffect(() = > {
const onUpdate = (e) = > {
const target = e.target;
const value = target.value;
setValue(value);
};
debouncedOnUpdateRef.current = debounce(onUpdate, 800);
return () = >{ debouncedOnUpdateRef.current.cancel(); }; } []);return (
<div className="App">
<input onKeyDown={debouncedOnUpdateRef.current} />
<p>{value}</p>
</div>
);
}
Copy the code
To implement adebounce
Here is a step-by-step implementation of Debounce using the LoDash source code as a reference.
Basic implementation
Start with an intuitive test scenario
Codepen. IO/curly210102…
Implement the base debounce function using clearTimeout + setTimeout, calling debounce → regenerating the timer → terminating the function on time.
function debounce(
func,
wait = 0,
options = {
leading: false,
trailing: true,
maxWait: 0,}) {
let timer = null;
let result = null;
function debounced(. args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() = > {
result = func.apply(this, args);
}, wait);
return result;
}
debounced.cancel = () = > {};
debounced.flush = () = > {};
return debounced;
}
Copy the code
Performance optimization
ClearTimeout + setTimeout does quickly implement basic functionality, but there is room for optimization.
For example, if you have an array, add elements by debouncedSort. By adding 100 consecutive elements, the debmentioning function will need to unload and reinstall the timer 100 times in a short time. There’s not much point in reinventing the wheel. In fact, a timer can do it.
Set a timer that runs its course undisturbed by the trigger operation. Calculate the difference between the expected duration and the actual duration, and set the next timer according to the difference.
function debounce(
func,
wait = 0,
options = {
leading: false,
trailing: true,
maxWait: 0,}) {
let timer = null;
let result = null;
let lastCallTime = 0;
let lastThis = null;
let lastArgs = null;
function later() {
const last = Date.now() - lastCallTime;
if (last >= wait) {
result = func.apply(lastThis, lastArgs);
timer = null;
} else {
timer = setTimeout(later, wait - last); }}function debounced(. args) {
lastCallTime = Date.now();
if(! timer) { timer =setTimeout(later, wait);
}
return result;
}
debounced.cancel = () = > {};
debounced.flush = () = > {};
return debounced;
}
Copy the code
Codepen. IO/curly210102…
Optimization and performance reference since modernjavascript.blogspot.com/2013/08/bui images…
Realize the options
Implement leading and trailing, find the “start” and “end” points in the code, and add the leading and trailing conditions.
Note that when both leading and trailing are true, the end is called only if the action is triggered multiple times during the wait. This is done by logging the most recent parameter, lastArgs.
function debounce(
func,
wait = 0,
options = {
leading: false,
trailing: true,
maxWait: 0,}) {
const leading = options.leading;
const trailing = options.trailing;
const hasMaxWait = "maxWait" in options;
const maxWait = Math.max(options.maxWait, wait);
let timer = null;
let result = null;
let lastCallTime = 0;
let lastThis = null;
let lastArgs = null;
function later() {
const last = Date.now() - lastCallTime;
if (last >= wait) {
/ / end
timer = null;
// Use lastArgs to determine if there are multiple triggers, leading: true, trailing: true
// This may be null/undefined cannot be used as a judgment object
if (trailing && lastArgs) {
invokeFunction();
}
lastArgs = lastThis = null;
} else {
timer = setTimeout(later, wait - last); }}function invokeFunction() {
const thisArg = lastThis;
const args = lastArgs;
lastArgs = lastThis = null;
result = func.apply(thisArg, args);
}
function debounced(. args) {
lastCallTime = Date.now();
lastThis = this;
lastArgs = args;
if (timer === null) {
/ / the start
timer = setTimeout(later, wait);
if(leading) { invokeFunction(); }}return result;
}
debounced.cancel = () = > {};
debounced.flush = () = > {};
return debounced;
}
Copy the code
Codepen. IO/curly210102…
We then implement maxWait, which refers to the maximum interval between func executions.
To implement maxWait, determine:
- How to judge reached
maxWait
- When to judge
How to judge reached?
Record the time lastInvokeTime when func was first invoked and last executed, judging date.now () -lastInvokeTime >= maxWait
When to judge?
MaxWait attempts to modify existing timers by using them to watch.
Since maxWait is greater than or equal to WAIT, the initial timer can use WAIT directly, but the duration of subsequent timers must take into account maxWait remaining time.
function remainingWait(time) {
// Remaining waiting time
const remainingWaitingTime = wait - (time - lastCallTime);
// 'func' The remaining time of the maximum delay
const remainingMaxWaitTime = maxWait - (time - lastInvokeTime);
return hasMaxWait
? Math.min(remainingWaitingTime, remainingMaxWaitTime)
: remainingWaitingTime;
}
Copy the code
At the same time, this change also brings a problem, the original delay was the remainingWait time, but now it becomes remainingWait. Three situations arise:
- Case one: The timer callback is
wait
point-to-point - Case 2: The timer callback is
maxWait
point-to-point
Note that the last case, trailing: false, does not execute func when maxWait reaches the point. Because at this point there is no way to determine if there are any subsequent operations, there is no way to determine if the most recent operation is trailing (the trailing: false tail is not executed), and you have to wait passively for the next call before executing func.
Focus on the two pointcuts for implementing func: “Calling debmentioning function” and “timer callback”.
Two pointcuts do not have absolute sequential relationship, can be said to operate in parallel with each other. One pointcut executes func and the other should skip execution. So add shouldInvoke judgment to all of them to avoid chaos.
function shouldInvoke(time) {
return (
time - lastCallTime >= wait ||
(hasMaxWait && time - lastInvokeTime >= maxWait)
);
}
Copy the code
The final code
function debounce(
func,
wait = 0,
options = {
leading: false,
trailing: true,
maxWait: 0,}) {
const leading = options.leading;
const trailing = options.trailing;
const hasMaxWait = "maxWait" in options;
const maxWait = Math.max(options.maxWait, wait);
let timer = null;
let result = null;
let lastCallTime = 0;
let lastInvokeTime = 0;
let lastThis = null;
let lastArgs = null;
function later() {
const time = Date.now();
if (shouldInvoke(time)) {
/ / end
timer = null;
if (trailing && lastArgs) {
invokeFunction(time);
}
lastArgs = lastThis = null;
} else {
timer = setTimeout(later, remainingTime(time));
}
return result;
}
function shouldInvoke(time) {
return (
time - lastCallTime >= wait ||
(hasMaxWait && time - lastInvokeTime >= maxWait)
);
}
function remainingTime(time) {
const last = time - lastCallTime;
const lastInvoke = time - lastInvokeTime;
const remainingWaitingTime = wait - last;
return hasMaxWait
? Math.min(remainingWaitingTime, maxWait - lastInvoke)
: remainingWaitingTime;
}
function invokeFunction(time) {
const thisArg = lastThis;
const args = lastArgs;
lastInvokeTime = time;
lastArgs = lastThis = null;
result = func.apply(thisArg, args);
}
function debounced(. args) {
const time = Date.now();
const needInvoke = shouldInvoke(time);
lastCallTime = time;
lastThis = this;
lastArgs = args;
if (needInvoke) {
if (timer === null) {
lastInvokeTime = time;
timer = setTimeout(later, wait);
if (leading) {
invokeFunction(time);
}
return result;
}
if (hasMaxWait) {
timer = setTimeout(later, wait);
invokeFunction(time);
returnresult; }}if (timer === null) {
timer = setTimeout(later, wait);
}
return result;
}
debounced.cancel = () = > {};
debounced.flush = () = > {};
return debounced;
}
Copy the code
Codepen. IO/curly210102…
Just to be briefthrottle
Debounce is used for shock prevention and throttle for throttling.
Throttling is performed at most once in a specified period of time.
Simple throttling implementation
function throttle(fn, wait) {
let lastCallTime = 0;
return function (. args) {
const time = Date.now();
if (time - lastCallTime >= wait) {
fn.apply(this, args);
}
lastCallTime = time;
};
}
Copy the code
function throttle(fn, wait) {
let isLocked = false;
return function (. args) {
if(! isLocked) {return;
}
isLocked = true;
fn.apply(this, args);
setTimeout(() = > {
isLocked = false;
}, wait);
};
}
Copy the code
Lodash uses Debounce for throttle
_.throttle(
func,
[(wait = 0)],
[
(options = {
leading: true.// Whether to execute before throttling starts
trailing: true.// Whether to execute after throttling ends}));Copy the code
function throttle(func, wait, options) {
let leading = true;
let trailing = true;
if (typeoffunc ! = ="function") {
throw new TypeError("Expected a function");
}
if (isObject(options)) {
leading = "leading" inoptions ? !!!!! options.leading : leading; trailing ="trailing" inoptions ? !!!!! options.trailing : trailing; }return debounce(func, wait, {
leading,
trailing,
maxWait: wait,
});
}
Copy the code