The problem
Need debounce functionality with immediate execution option
The solution
function debounce(func, wait, options = {}) {
const { leading = false, trailing = true, maxWait } = options;
let timeoutId, lastCallTime, lastInvokeTime, result;
let lastArgs, lastThis;
function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge(time) {
lastInvokeTime = time;
if (leading) {
return invokeFunc(time);
}
return result;
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait - timeSinceLastCall;
return maxWait ?
Math.min(timeWaiting, maxWait - timeSinceLastInvoke) :
timeWaiting;
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timerId = setTimeout(timerExpired, remainingWait(time));
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return (
lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxWait !== undefined && timeSinceLastInvoke >= maxWait)
);
}
function trailingEdge(time) {
timeoutId = undefined;
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timeoutId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxWait !== undefined) {
clearTimeout(timeoutId);
timeoutId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timeoutId === undefined) {
timeoutId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = function() {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timeoutId = undefined;
};
debounced.flush = function() {
return timeoutId === undefined ? result : trailingEdge(Date.now());
};
return debounced;
}Parameters
funcfunctionFunction to debounce
waitnumberWait time in milliseconds
optionsobjectOptions object with leading, trailing, maxWait
Put it to work
// Debounce with immediate execution
const debouncedSearch = debounce(
(query) => {
console.log('Searching for:', query);
},
300,
{ leading: true, trailing: true }
);
// Will execute immediately, then wait
debouncedSearch('hello');
debouncedSearch('world');
// Cancel debounced call
debouncedSearch.cancel();
// Force execution
debouncedSearch.flush();