diff --git a/coding-exercise/throttle-function/README.md b/coding-exercise/throttle-function/README.md new file mode 100644 index 00000000..0bd7f0a9 --- /dev/null +++ b/coding-exercise/throttle-function/README.md @@ -0,0 +1,304 @@ +# Throttle Function + +## Challenge +Implement a throttle function that limits the rate at which a callback can execute, ensuring it runs at most once per specified time interval. + +## Problem Description +Throttling is a technique used to control the rate at which a function executes. Unlike debouncing (which delays execution until activity stops), throttling ensures a function executes at regular intervals during continuous activity. + +### Real-World Use Cases +- **Scroll Events**: Update UI elements (like progress bars or lazy-loading) at a controlled rate while scrolling +- **Mouse Movement**: Track cursor position without overwhelming the browser with updates +- **Window Resize**: Recalculate layout at regular intervals during resize +- **API Rate Limiting**: Ensure requests don't exceed API rate limits +- **Game Development**: Limit action frequency (e.g., shooting, jumping) to prevent spam +- **Auto-save**: Save user input at regular intervals while they're typing +- **Infinite Scroll**: Load more content at controlled intervals while scrolling + +## Example + +### Input +```js +function updateScrollPosition(position) { + console.log(`Scroll position: ${position}px`); +} + +const throttledUpdate = throttle(updateScrollPosition, 1000); + +// User scrolls continuously +throttledUpdate(100); // t=0ms +throttledUpdate(200); // t=100ms +throttledUpdate(300); // t=200ms +throttledUpdate(400); // t=300ms +throttledUpdate(500); // t=1100ms +throttledUpdate(600); // t=1200ms +``` + +### Output +``` +// Executes at regular 1000ms intervals +Scroll position: 100px // t=0ms (immediate) +Scroll position: 500px // t=1100ms (after 1000ms) +Scroll position: 600px // t=2200ms (after another 1000ms) +``` + +## Requirements +1. The throttle function should accept a function and a time limit +2. It should return a new function that limits execution rate +3. The function should execute immediately on the first call (leading edge) +4. Subsequent calls within the time limit should be controlled +5. The function should preserve the correct `this` context and arguments +6. Should handle both leading and trailing edge execution options + +## Key Concepts +- **Closures**: Maintaining state (lastExecuted, timeoutId) across function calls +- **Higher-Order Functions**: Returning a function from a function +- **setTimeout/clearTimeout**: Managing asynchronous delays +- **Function Context**: Using `apply()` to preserve `this` binding +- **Timestamps**: Using `Date.now()` to track execution timing + +## Implementation Approaches + +### 1. Leading Edge Throttle +Executes immediately on first call, then ignores calls for the limit duration: +```js +function throttleLeading(func, limit) { + let inThrottle = false; + + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} +``` + +### 2. Leading + Trailing Edge Throttle +Executes immediately and also schedules a final execution: +```js +function throttle(func, limit) { + let lastExecuted = 0; + let timeoutId = null; + + return function(...args) { + const now = Date.now(); + const timeSinceLastExecution = now - lastExecuted; + + if (timeSinceLastExecution >= limit) { + lastExecuted = now; + func.apply(this, args); + } else { + clearTimeout(timeoutId); + const remainingTime = limit - timeSinceLastExecution; + timeoutId = setTimeout(() => { + lastExecuted = Date.now(); + func.apply(this, args); + }, remainingTime); + } + }; +} +``` + +## Throttle vs Debounce + +| Feature | Throttle | Debounce | +|---------|----------|----------| +| **Execution Pattern** | At regular intervals during activity | Only after activity stops | +| **Frequency** | Guaranteed execution every X ms | Single execution after silence | +| **Use Case** | Continuous updates (scroll, resize) | Wait for completion (search input) | +| **Example** | Update scroll position every 100ms | Search API call 500ms after typing stops | +| **Behavior** | Executes periodically while active | Executes once when idle | + +### Visual Comparison +``` +User Activity: ████████████████████████████████ + ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ + +Throttle: ✓ ✓ ✓ ✓ + (executes at regular intervals) + +Debounce: ✓ + (executes only after activity stops) +``` + +## Benefits +- **Performance**: Reduces unnecessary function calls during high-frequency events +- **Consistency**: Ensures predictable execution intervals +- **User Experience**: Provides smooth, responsive updates without lag +- **Resource Management**: Prevents overwhelming the browser or server +- **Battery Life**: Reduces CPU usage on mobile devices + +## Common Pitfalls + +### 1. Losing `this` Context +```js +// ❌ Wrong: Arrow function loses context +function throttle(func, limit) { + return () => func(); // 'this' is lost +} + +// ✓ Correct: Use regular function and apply +function throttle(func, limit) { + return function(...args) { + func.apply(this, args); // Preserves 'this' + }; +} +``` + +### 2. Not Clearing Previous Timeouts +```js +// ❌ Wrong: Multiple timeouts can stack up +function throttle(func, limit) { + return function() { + setTimeout(() => func(), limit); // Creates new timeout every call + }; +} + +// ✓ Correct: Clear previous timeout +function throttle(func, limit) { + let timeoutId; + return function() { + clearTimeout(timeoutId); // Clear previous + timeoutId = setTimeout(() => func(), limit); + }; +} +``` + +### 3. Forgetting to Pass Arguments +```js +// ❌ Wrong: Arguments are lost +function throttle(func, limit) { + return function() { + func(); // No arguments passed + }; +} + +// ✓ Correct: Collect and pass arguments +function throttle(func, limit) { + return function(...args) { + func.apply(this, args); // Pass all arguments + }; +} +``` + +## Interview Tips + +### Questions You Might Be Asked +1. **"What's the difference between throttle and debounce?"** + - Throttle: Regular intervals during activity + - Debounce: Single execution after activity stops + +2. **"When would you use throttle over debounce?"** + - Use throttle for continuous updates (scroll, resize, mouse move) + - Use debounce for completion-based actions (search, form validation) + +3. **"How would you implement leading vs trailing edge?"** + - Leading: Execute immediately, ignore subsequent calls + - Trailing: Schedule execution for end of interval + - Both: Execute immediately and schedule final call + +4. **"What are the performance benefits?"** + - Reduces function calls (e.g., from 1000/sec to 10/sec) + - Prevents browser lag and jank + - Reduces API calls and server load + +5. **"How do you preserve the `this` context?"** + - Use regular function (not arrow function) + - Use `func.apply(this, args)` to call original function + +### Code Review Points +- ✓ Preserves `this` context with `apply()` +- ✓ Passes all arguments with rest/spread operators +- ✓ Clears previous timeouts to prevent memory leaks +- ✓ Uses closures correctly to maintain state +- ✓ Handles edge cases (first call, rapid calls, etc.) + +## Advanced Variations + +### Throttle with Options +```js +function throttle(func, limit, options = {}) { + const { leading = true, trailing = true } = options; + let lastExecuted = 0; + let timeoutId = null; + + return function(...args) { + const now = Date.now(); + + if (!lastExecuted && !leading) { + lastExecuted = now; + } + + const remaining = limit - (now - lastExecuted); + + if (remaining <= 0) { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + lastExecuted = now; + func.apply(this, args); + } else if (!timeoutId && trailing) { + timeoutId = setTimeout(() => { + lastExecuted = leading ? Date.now() : 0; + timeoutId = null; + func.apply(this, args); + }, remaining); + } + }; +} +``` + +### Throttle with Cancel Method +```js +function throttle(func, limit) { + let timeoutId = null; + let lastExecuted = 0; + + const throttled = function(...args) { + const now = Date.now(); + const remaining = limit - (now - lastExecuted); + + if (remaining <= 0) { + lastExecuted = now; + func.apply(this, args); + } + }; + + // Add cancel method + throttled.cancel = function() { + clearTimeout(timeoutId); + timeoutId = null; + lastExecuted = 0; + }; + + return throttled; +} +``` + +## Testing Considerations +```js +// Test basic throttling +const mockFn = jest.fn(); +const throttled = throttle(mockFn, 1000); + +throttled(); // Should execute +throttled(); // Should be ignored +jest.advanceTimersByTime(1000); +throttled(); // Should execute + +expect(mockFn).toHaveBeenCalledTimes(2); +``` + +## Related Patterns +- **Debounce**: Delays execution until activity stops +- **Rate Limiting**: Restricts number of calls in a time window +- **Request Animation Frame**: Browser-optimized throttling for animations +- **Web Workers**: Offload heavy computations to background threads + +## Further Reading +- [MDN: Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) +- [Lodash throttle implementation](https://lodash.com/docs/#throttle) +- [CSS Tricks: Debouncing and Throttling](https://css-tricks.com/debouncing-throttling-explained-examples/) diff --git a/coding-exercise/throttle-function/throttle.js b/coding-exercise/throttle-function/throttle.js new file mode 100644 index 00000000..44468459 --- /dev/null +++ b/coding-exercise/throttle-function/throttle.js @@ -0,0 +1,213 @@ +/** + * Creates a throttled version of a function that only executes at most once + * per specified time interval, regardless of how many times it's called. + * + * @param {Function} func - The function to throttle + * @param {number} limit - The time interval in milliseconds between allowed executions + * @returns {Function} A throttled version of the original function + * + * @example + * const throttledFn = throttle(() => console.log('Hello'), 1000); + * throttledFn(); // Executes immediately + * throttledFn(); // Ignored (within 1000ms) + * throttledFn(); // Ignored (within 1000ms) + * // After 1000ms, the next call will execute + */ +function throttle(func, limit) { + // Track the last time the function was executed + // This variable persists across multiple calls due to closure + let lastExecuted = 0; + + // Track if there's a pending execution scheduled + let timeoutId = null; + + // Return a new function that wraps the original function + // ...args collects all arguments passed to this function + return function(...args) { + // Get the current timestamp + const now = Date.now(); + + // Calculate how much time has passed since last execution + const timeSinceLastExecution = now - lastExecuted; + + // If enough time has passed, execute immediately + if (timeSinceLastExecution >= limit) { + // Update the last execution time + lastExecuted = now; + + // Execute the original function with correct context and arguments + // func.apply(this, args) ensures: + // 1. 'this' context is preserved (important for object methods) + // 2. All arguments are passed to the original function + func.apply(this, args); + } else { + // If not enough time has passed, schedule execution for later + // Clear any existing scheduled execution + clearTimeout(timeoutId); + + // Calculate remaining time until next allowed execution + const remainingTime = limit - timeSinceLastExecution; + + // Schedule the function to execute after the remaining time + timeoutId = setTimeout(() => { + lastExecuted = Date.now(); + func.apply(this, args); + }, remainingTime); + } + }; +} + +// ============================================ +// ALTERNATIVE IMPLEMENTATION: Leading Edge Only +// ============================================ + +/** + * Simpler throttle implementation that only executes on the leading edge + * (executes immediately, then ignores calls for the limit duration) + * + * @param {Function} func - The function to throttle + * @param {number} limit - The time interval in milliseconds between allowed executions + * @returns {Function} A throttled version of the original function + */ +function throttleLeading(func, limit) { + let inThrottle = false; + + return function(...args) { + // If not currently throttled, execute the function + if (!inThrottle) { + // Execute the function immediately + func.apply(this, args); + + // Set throttle flag to true + inThrottle = true; + + // Reset the throttle flag after the limit duration + setTimeout(() => { + inThrottle = false; + }, limit); + } + // If in throttle period, ignore the call + }; +} + +// ============================================ +// EXAMPLE USAGE: Scroll Event Handler +// ============================================ + +/** + * Simulates a scroll position tracker + * In a real application, this might update UI elements based on scroll position + * + * @param {number} position - The current scroll position + */ +function handleScroll(position) { + console.log(`Scroll position: ${position}px`); + // In production, this might be: + // - Updating a progress bar + // - Loading more content (infinite scroll) + // - Showing/hiding navigation elements +} + +// Create a throttled version of handleScroll with 1000ms limit +// This means the handler will execute at most once per second +const throttledScroll = throttle(handleScroll, 1000); + +// ============================================ +// DEMONSTRATION: Simulating Rapid Scroll Events +// ============================================ + +console.log('User scrolls rapidly...\n'); + +// Simulate rapid scroll events (in reality, scroll events fire very frequently) +// With throttling, only some of these will actually execute +throttledScroll(100); // Executes immediately (first call) +console.log('Called at 0ms\n'); + +setTimeout(() => { + throttledScroll(200); // Ignored (within 1000ms) + console.log('Called at 200ms (ignored)\n'); +}, 200); + +setTimeout(() => { + throttledScroll(300); // Ignored (within 1000ms) + console.log('Called at 400ms (ignored)\n'); +}, 400); + +setTimeout(() => { + throttledScroll(400); // Ignored (within 1000ms) + console.log('Called at 600ms (ignored)\n'); +}, 600); + +setTimeout(() => { + throttledScroll(500); // Ignored (within 1000ms) + console.log('Called at 800ms (ignored)\n'); +}, 800); + +setTimeout(() => { + throttledScroll(600); // Executes (1000ms has passed) + console.log('Called at 1200ms (executes)\n'); +}, 1200); + +setTimeout(() => { + throttledScroll(700); // Ignored (within 1000ms of last execution) + console.log('Called at 1400ms (ignored)\n'); +}, 1400); + +setTimeout(() => { + throttledScroll(800); // Executes (1000ms has passed since last execution) + console.log('Called at 2400ms (executes)\n'); +}, 2400); + +// Expected output: +// Scroll position: 100px (at 0ms - immediate) +// Scroll position: 600px (at ~1200ms - after 1000ms interval) +// Scroll position: 800px (at ~2400ms - after another 1000ms interval) +// +// Without throttling, handleScroll would have been called 8 times! +// With throttling, it's called only 3 times - reducing unnecessary executions by 62.5% + +/** + * Explanation: + * + * This demonstrates the throttle pattern in JavaScript. + * + * 1. Throttling limits the rate at which a function can execute. + * The function will execute at most once per specified time interval. + * + * 2. When throttledScroll is called multiple times rapidly, + * it will only execute once per 1000ms (1 second). + * + * 3. The first call executes immediately (leading edge). + * Subsequent calls within the time limit are either ignored or scheduled. + * + * 4. Common use cases: + * - Scroll events: Update UI elements without overwhelming the browser + * - Mouse move: Track cursor position without excessive updates + * - Window resize: Recalculate layout at a controlled rate + * - API rate limiting: Ensure requests don't exceed API limits + * - Game loop: Limit frame rate or action frequency + * + * 5. Benefits: + * - Improves performance by limiting function execution frequency + * - Prevents browser lag from excessive event handlers + * - Ensures consistent execution intervals + * - Reduces server load from API calls + * + * 6. The function uses closures to maintain lastExecuted and timeoutId across calls. + * + * 7. func.apply(this, args) ensures the original function is called with + * the correct context and arguments. + * + * THROTTLE vs DEBOUNCE: + * + * - **Throttle**: Executes the function at regular intervals (e.g., once per second) + * regardless of how many times it's called. Guarantees execution at a steady rate. + * Example: Scroll handler that updates every 100ms while scrolling. + * + * - **Debounce**: Executes the function only after calls have stopped for a specified + * period. Delays execution until activity ceases. + * Example: Search input that waits 500ms after user stops typing. + * + * Use throttle when you want regular updates during continuous activity. + * Use debounce when you want to wait for activity to stop. + */