Hone logo
Hone
Problems

Implement Debounce with Leading Edge in JavaScript

You're tasked with creating a robust debounce function in JavaScript that includes a "leading edge" option. Debouncing is a common technique used to limit the rate at which a function can be invoked. This is particularly useful for events that fire rapidly, such as resizing a window or typing in a search bar. The leading edge option means the function should be called immediately on the first invocation within a given time frame, and then subsequent calls within that time frame should be ignored until the time frame has passed.

Problem Description

Implement a JavaScript function called debounce that takes two arguments:

  1. func: The function to debounce.
  2. wait: The number of milliseconds to delay.
  3. options: An optional object. If options.leading is true, the function should be invoked on the leading edge. If options.leading is false or not provided, the function should only be invoked on the trailing edge (the default debounce behavior).

The debounce function should return a new function that, when invoked, will manage the timing of func.

Key Requirements:

  • Leading Edge Execution: When options.leading is true, the func should be executed immediately upon the first invocation of the debounced function within any given wait period.
  • Trailing Edge Execution (Default): If options.leading is false or not provided, the func should only be executed after the wait period has elapsed since the last invocation of the debounced function.
  • Arguments and this Context: The debounced function should pass along any arguments it receives to the original func. It should also preserve the this context in which it was called.
  • Return Value: The debounced function should return the result of the last successful invocation of func. If func hasn't been invoked yet, or if it's invoked on the leading edge and subsequent calls are suppressed, the return value might be undefined.

Expected Behavior:

  • Leading Edge (leading: true):

    • The first time the debounced function is called, func executes immediately.
    • If the debounced function is called again before wait milliseconds have passed, func is not executed again.
    • After wait milliseconds have passed since the last invocation, the debounced function can be called again, and func will execute immediately on this new invocation.
  • Trailing Edge (leading: false or omitted):

    • The first time the debounced function is called, a timer is set.
    • If the debounced function is called again before wait milliseconds have passed, the timer is reset.
    • func is executed only after wait milliseconds have passed since the last invocation.

Edge Cases:

  • Rapid Invocations: Handle scenarios where the debounced function is called many times in quick succession.
  • Zero Wait Time: Consider the behavior when wait is 0.
  • No Arguments: Ensure the function works correctly when called with no arguments.
  • this Context: Verify that this is correctly bound when func is invoked.

Examples

Example 1: Leading Edge Enabled

Let's say wait is 100ms and options.leading is true.

const log = (message) => console.log(message);
const debouncedLog = debounce(log, 100, { leading: true });

console.log("Calling immediately 1");
debouncedLog("Call 1"); // Output: "Call 1" (immediately)

setTimeout(() => {
  console.log("Calling after 50ms");
  debouncedLog("Call 2"); // No output, suppressed
}, 50);

setTimeout(() => {
  console.log("Calling after 150ms");
  debouncedLog("Call 3"); // Output: "Call 3" (immediately, as 100ms passed since Call 1)
}, 150);

setTimeout(() => {
  console.log("Calling after 200ms");
  debouncedLog("Call 4"); // No output, suppressed (within 100ms of Call 3)
}, 200);

setTimeout(() => {
  console.log("Calling after 300ms");
  debouncedLog("Call 5"); // Output: "Call 5" (immediately, as 100ms passed since Call 3)
}, 300);

Expected Output:

Calling immediately 1
Call 1
Calling after 50ms
Calling after 150ms
Call 3
Calling after 200ms
Calling after 300ms
Call 5

Example 2: Trailing Edge Enabled (Default Behavior)

Let's say wait is 100ms and options.leading is false (or omitted).

const log = (message) => console.log(message);
const debouncedLog = debounce(log, 100); // leading is false by default

console.log("Calling immediately 1");
debouncedLog("Call 1"); // No output yet

setTimeout(() => {
  console.log("Calling after 50ms");
  debouncedLog("Call 2"); // Timer reset, no output yet
}, 50);

setTimeout(() => {
  console.log("Calling after 150ms");
  debouncedLog("Call 3"); // Timer reset, no output yet
}, 150);

setTimeout(() => {
  console.log("Final call after 300ms");
  // After 100ms from Call 3 (i.e., at 250ms), func would normally execute.
  // This call at 300ms resets the timer, so func will execute 100ms after this.
  debouncedLog("Call 4"); // No output yet
}, 300);

// We expect "Call 3" to be logged around 250ms if no further calls.
// With the last call at 300ms, "Call 4" will be logged around 400ms.
// For demonstration, let's assume this is the last event.
setTimeout(() => {
  console.log("--- End of events ---");
}, 500);

Expected Output:

Calling immediately 1
Calling after 50ms
Calling after 150ms
Final call after 300ms
// (Approx 400ms later)
Call 4

Note: The exact timing of "Call 4" depends on when the last timer fires. The important part is that it fires after the wait period from the last invocation.

Example 3: this Context and Arguments

class Counter {
  constructor() {
    this.count = 0;
  }

  increment(step) {
    this.count += step;
    console.log(`Counter incremented by ${step}. New count: ${this.count}`);
  }
}

const myCounter = new Counter();
const debouncedIncrement = debounce(myCounter.increment, 100, { leading: true });

console.log("Calling debouncedIncrement with leading edge");
debouncedIncrement.call(myCounter, 5); // Should log "Counter incremented by 5. New count: 5" immediately.

setTimeout(() => {
  debouncedIncrement.call(myCounter, 10); // Suppressed
}, 50);

setTimeout(() => {
  debouncedIncrement.call(myCounter, 2); // Should log "Counter incremented by 2. New count: 7" immediately.
}, 150);

Expected Output:

Calling debouncedIncrement with leading edge
Counter incremented by 5. New count: 5
Counter incremented by 2. New count: 7

Constraints

  • The wait parameter will be a non-negative integer representing milliseconds.
  • func will be a valid JavaScript function.
  • options will be an optional object, and if present, options.leading will be a boolean.
  • The debounced function should handle up to 1000 rapid invocations without significant performance degradation.

Notes

  • Consider using setTimeout and clearTimeout for managing the delay.
  • You'll need a way to track whether a timeout is currently active.
  • For the leading edge, you'll also need to track whether the function has already been invoked within the current wait period.
  • Remember to handle the this context and arguments correctly. The apply or call methods of functions can be helpful here.
  • Think about what happens when wait is 0. It should essentially behave like calling the function directly, but still respect the leading/trailing logic.
Loading editor...
javascript