Welcome to Michael's Blog! 🎉 currently Renew and moving old post 🫠

Throttle and Debounce

Implement Throttle and Debounce Functions

Loading comments...

Throttle and Debounce Implementation

難度:Easy

題目描述

實作 throttle 和 debounce 函數,需要符合以下使用方式:
type Callback = (...args: any[]) => void;

function throttle(callback: Callback, delay: number): Callback;
function debounce(callback: Callback, delay: number): Callback;

// 使用範例
const throttledScroll = throttle(() => console.log('scroll'), 1000);
const debouncedResize = debounce(() => console.log('resize'), 1000);

window.addEventListener('scroll', throttledScroll);
window.addEventListener('resize', debouncedResize);
Performance

解題思路

1. 需求分析

這題要求我們實作兩個效能優化的函數:throttle(節流)和 debounce(防抖),用於處理高頻率事件。

  1. Throttle(節流)
    • 在一定時間內,只執行一次函數
    • 適合處理持續性事件,例如視窗滾動
  2. Debounce(防抖)
    • 在最後一次觸發後的一定時間才執行
    • 適合處理最終狀態,例如搜尋輸入

2. 實作步驟

首先是 Throttle 的實作:

throttle
type Callback = (...args: any[]) => void;

export function throttle(callback: Callback, delay: number): Callback {
  let timerID: NodeJS.Timeout | null = null;

  return function (this: unknown, ...args) {
    if (timerID)
      return;

    timerID = setTimeout(() => {
      callback.apply(this, args);
      timerID = null;
    }, delay);
  };
}

接著是 Debounce 的實作:

debounce
type Callback = (...args: any[]) => void;

export function debounce(callback: Callback, delay: number): Callback {
  let timerID: NodeJS.Timeout | null = null;

  return function (this: unknown, ...args) {
    if (timerID) {
      clearTimeout(timerID);
    }

    timerID = setTimeout(() => {
      callback.apply(this, args);
    }, delay);
  };
}

使用範例

1. 滾動事件優化

const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 1000);

window.addEventListener('scroll', handleScroll);

2. 搜尋輸入優化

const handleSearch = debounce(async (query: string) => {
  const results = await searchAPI(query);
  updateResults(results);
}, 500);

searchInput.addEventListener('input', (e) => {
  handleSearch(e.target.value);
});

測試案例

throttle test
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { throttle } from './solution';

describe('throttle', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.clearAllTimers();
  });

  it('should limit function calls', () => {
    const callback = vi.fn();
    const throttledFn = throttle(callback, 1000);

    throttledFn();
    throttledFn(); // 應該被忽略
    throttledFn(); // 應該被忽略

    vi.advanceTimersByTime(1000);
    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('should preserve this context', () => {
    const context = { value: 42 };
    const callback = vi.fn(function (this: typeof context) {
      expect(this.value).toBe(42);
    });

    const throttledFn = throttle(callback, 1000);
    throttledFn.call(context);

    vi.advanceTimersByTime(1000);
    expect(callback).toHaveBeenCalledTimes(1);
  });
});
debounce test
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { debounce } from './solution';

describe('debounce', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.clearAllTimers();
  });

  it('should delay execution', () => {
    const callback = vi.fn();
    const debouncedFn = debounce(callback, 1000);

    debouncedFn();
    debouncedFn();
    debouncedFn();

    expect(callback).not.toHaveBeenCalled();
    vi.advanceTimersByTime(1000);
    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('should cancel previous timer', () => {
    const callback = vi.fn();
    const debouncedFn = debounce(callback, 1000);

    debouncedFn();
    vi.advanceTimersByTime(500);
    debouncedFn(); // 重置計時器
    vi.advanceTimersByTime(500);

    expect(callback).not.toHaveBeenCalled();
    vi.advanceTimersByTime(500);
    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('should preserve arguments', () => {
    const callback = vi.fn();
    const debouncedFn = debounce(callback, 1000);

    debouncedFn('test');
    vi.advanceTimersByTime(1000);

    expect(callback).toHaveBeenCalledWith('test');
  });
});

注意事項

  1. 使用場景
    • Throttle:滾動、調整大小等持續性事件
    • Debounce:搜尋、表單驗證等最終狀態事件
  2. 記憶體管理
    • 記得清理計時器避免記憶體洩漏
Loading comments...