Lodash Debounce and Defer Execution Timeline
Combining Lodash's _.debounce and _.defer
coordinates JavaScript's asynchronous event loop with timer-based
threshold delays. This article examines the exact step-by-step execution
timeline when merging these two utilities, detailing the behavior of the
call stack, the macro-task queue, and the internal timers according to
which function wraps the other.
Core Mechanics of Each Utility
To understand the combined timeline, consider their individual mechanics:
_.defer(func, [args]): Defers execution offuncuntil the current synchronous call stack clears. Internally, Lodash delegates_.defertosetTimeout(fn, 1)(or0depending on the environment), pushing the execution callback to the macro-task queue._.debounce(func, [wait=0], [options={}]): Delays the invocation offuncuntil after a specified duration (wait) has elapsed since the last time the debounced function was invoked. It maintains internal timestamps and sets a timer to evaluate execution eligibility.
Scenario
A: Deferring a Debounced Function
(_.defer(_.debounce(fn, wait)))
In this scenario, a debounced instance is scheduled using
_.defer:
const debouncedFn = _.debounce(targetFn, 100);
_.defer(debouncedFn);Step-by-Step Execution Timeline:
Tick 0 (Synchronous Execution - Call Stack):
_.debounce(targetFn, 100)initializes and returns the debounced wrapper._.defer(debouncedFn)runs. It registersdebouncedFnto the browser or Node.js macro-task timer queue with a minimum delay (typically 0–1ms).- Synchronous execution continues until the call stack is completely empty.
Tick 1 (Current Stack Cleared - Macro-Task Resolution):
- The event loop picks the deferred task from the macro-task queue.
debouncedFnis executed.- Rather than executing
targetFn, the debounced function records the timestamp (lastCallTime = now()) and schedules an internal timer (setTimeout) configured for thewaitperiod (100ms). - The call stack clears again.
Intermediary Ticks (Idle / Non-blocking):
- If no further calls are made to
debouncedFn, the 100ms timer runs in the background. Other micro-tasks and macro-tasks continue to process normally.
- If no further calls are made to
Tick 2 (Debounce Duration Met - Final Invocation):
- At \(t \ge \text{Tick 1 timestamp} + 100\text{ms}\), the debounce timer fires.
- The timer callback pushes the invocation of
targetFnto the macro-task queue. - The event loop executes
targetFnwith the trailing arguments.
Total Delay: Call stack clear time + \(\approx 1\text{ms}\) (defer) +
\(100\text{ms}\)
(wait).
Scenario
B: Debouncing a Deferred Function (_.debounce(..., wait)
containing _.defer)
In this scenario, the debounced function wraps a call to
_.defer:
const debouncedAndDeferred = _.debounce(() => {
_.defer(targetFn);
}, 100);
debouncedAndDeferred();Step-by-Step Execution Timeline:
Tick 0 (Synchronous Execution - Call Stack):
debouncedAndDeferred()is invoked synchronously.- The debounce logic sets an internal timer for 100ms.
- Synchronous code continues running; the call stack clears.
Tick 1 (Debounce Timer Resolves):
- After 100ms of inactivity, the debounce timer completes.
- The wrapper macro-task runs on the call stack.
- Inside the wrapper,
_.defer(targetFn)is called. targetFnis placed into the macro-task queue (scheduled viasetTimeout(..., 1)).- The wrapper finishes execution, and the call stack clears.
Tick 2 (Deferred Macro-Task Resolves):
- Any pending micro-tasks resolve first.
- The event loop pulls the deferred callback containing
targetFnfrom the macro-task queue. targetFnexecutes.
Total Delay: \(100\text{ms}\) (wait) + Call
stack clear time + \(\approx
1\text{ms}\) (defer).
Key Behavioral Differences
| Characteristic | _.defer(_.debounce(fn, wait)) |
_.debounce(() => _.defer(fn), wait) |
|---|---|---|
| Timer Start Point | After current stack clears (Macro-task 1) | Immediately upon debounced function call |
| Function Deferred | The scheduling wrapper | The actual target execution
(fn) |
| Leading Edge Effects | If leading: true, executes
right after call stack clears |
If leading: true,
_.defer fires immediately, executing fn on the
very next tick |