TS封装一个异步防抖函数,解决你在代码中的困难,很值得学习

82 阅读4分钟

异步防抖函数详解

下面我将详细解释这段异步防抖函数的实现原理和工作机制:

static asyncDebounce<T extends (...args: any[]) => Promise<R>, R>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => Promise<R> {
  // 状态变量
  let timer: number | null = null;
  let pendingPromise: Promise<R> | null = null;
  let pendingResolve: ((value: R) => void) | null = null;
  let pendingReject: ((reason?: any) => void) | null = null;
  let lastArgs: Parameters<T> | null = null;

  return async (...args: Parameters<T>): Promise<R> => {
    // 保存最新参数
    lastArgs = args;
    
    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }
    
    // 如果已有等待中的 Promise,拒绝它(表示被新调用取代)
    if (pendingPromise) {
      if (pendingReject) {
        pendingReject(new Error("Debounced call superseded"));
      }
      pendingPromise = null;
    }
    
    // 创建新的 Promise
    pendingPromise = new Promise<R>((resolve, reject) => {
      pendingResolve = resolve;
      pendingReject = reject;
    });
    
    // 设置新的定时器
    timer = setTimeout(async () => {
      try {
        // 执行实际函数
        const result = await fn(...(lastArgs as Parameters<T>));
        
        // 解析 Promise
        if (pendingResolve) {
          pendingResolve(result);
        }
      } catch (error) {
        // 拒绝 Promise
        if (pendingReject) {
          pendingReject(error);
        }
      } finally {
        // 清理状态
        timer = null;
        pendingPromise = null;
        pendingResolve = null;
        pendingReject = null;
        lastArgs = null;
      }
    }, delay);
    
    return pendingPromise;
  };
}

工作原理详解

1. 函数签名和泛型参数

static asyncDebounce<T extends (...args: any[]) => Promise<R>, R>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => Promise<R>
  • T:表示一个函数类型,该函数接受任意参数并返回一个 Promise
  • R:表示 Promise 解析后的值的类型
  • fn:需要防抖处理的异步函数
  • delay:防抖延迟时间(毫秒)
  • 返回值:一个新的函数,具有相同的参数类型,返回一个 Promise

2. 状态变量

let timer: number | null = null;
let pendingPromise: Promise<R> | null = null;
let pendingResolve: ((value: R) => void) | null = null;
let pendingReject: ((reason?: any) => void) | null = null;
let lastArgs: Parameters<T> | null = null;
  • timer:存储 setTimeout 的 ID,用于取消定时器
  • pendingPromise:当前挂起的 Promise
  • pendingResolve:用于解决当前 Promise 的函数
  • pendingReject:用于拒绝当前 Promise 的函数
  • lastArgs:保存最后一次调用的参数

3. 返回的防抖函数

当调用防抖函数时:

return async (...args: Parameters<T>): Promise<R> => {
  // 1. 保存最新参数
  lastArgs = args;
  
  // 2. 清除之前的定时器
  if (timer) {
    clearTimeout(timer);
  }
  
  // 3. 取消前一个未决的Promise
  if (pendingPromise) {
    if (pendingReject) {
      pendingReject(new Error("Debounced call superseded"));
    }
    pendingPromise = null;
  }
  
  // 4. 创建新的Promise
  pendingPromise = new Promise<R>((resolve, reject) => {
    pendingResolve = resolve;
    pendingReject = reject;
  });
  
  // 5. 设置新的定时器
  timer = setTimeout(async () => {
    // 定时器到期后的操作...
  }, delay);
  
  // 6. 返回新创建的Promise
  return pendingPromise;
};

4. 定时器回调(防抖核心)

当定时器到期后执行:

timer = setTimeout(async () => {
  try {
    // 执行实际函数
    const result = await fn(...(lastArgs as Parameters<T>));
    
    // 解析Promise
    if (pendingResolve) {
      pendingResolve(result);
    }
  } catch (error) {
    // 拒绝Promise
    if (pendingReject) {
      pendingReject(error);
    }
  } finally {
    // 清理状态
    timer = null;
    pendingPromise = null;
    pendingResolve = null;
    pendingReject = null;
    lastArgs = null;
  }
}, delay);

工作流程可视化

graph TD
    A[调用防抖函数] --> B[保存参数 lastArgs]
    B --> C[清除旧定时器]
    C --> D[取消前一个Promise]
    D --> E[创建新Promise]
    E --> F[设置新定时器]
    F --> G[返回新Promise]
    
    G --> H{等待 delay 毫秒}
    
    H -- 期间有新调用 --> B
    H -- 无新调用 --> I[执行原始函数 fn]
    I --> J[成功]
    J --> K[解析Promise]
    I --> L[失败]
    L --> M[拒绝Promise]
    K & M --> N[清理所有状态]

防抖过程详解

  1. 首次调用

    • 保存参数
    • 创建新的 Promise
    • 设置定时器
    • 返回 Promise
  2. 在 delay 时间内再次调用

    • 清除之前的定时器
    • 拒绝之前的 Promise(通知它被取代)
    • 保存新参数
    • 创建新的 Promise
    • 设置新的定时器
    • 返回新的 Promise
  3. delay 时间内无新调用

    • 定时器到期
    • 执行原始函数 fn
    • 根据执行结果解决或拒绝 Promise
    • 清理所有状态变量

关键特性

  1. 参数更新

    • 总是使用最后一次调用的参数
    • 中间调用的参数会被丢弃
  2. Promise 管理

    • 每次调用返回新的 Promise
    • 当被新调用取代时,前一个 Promise 会被拒绝
    • 只有最后一次调用的 Promise 会被解决
  3. 错误处理

    • 原始函数抛出的错误会传递到 Promise
    • 被取代的 Promise 收到特定错误
  4. 资源清理

    • 执行完成后重置所有状态
    • 避免内存泄漏

使用示例

// 模拟一个异步API调用
async function fetchData(query: string): Promise<string[]> {
  console.log(`Fetching data for: ${query}`);
  // 模拟网络请求
  await new Promise(resolve => setTimeout(resolve, 200));
  return [`result1 for ${query}`, `result2 for ${query}`];
}

// 创建防抖版本
const debouncedFetch = Utility.asyncDebounce(fetchData, 500);

// 模拟用户连续输入
async function simulateUserInput() {
  try {
    const results = await debouncedFetch("a");
    console.log("Results for 'a':", results);
  } catch (err) {
    if (err.message === "Debounced call superseded") {
      console.log("Request for 'a' was superseded");
    }
  }
  
  try {
    const results = await debouncedFetch("ab");
    console.log("Results for 'ab':", results);
  } catch (err) {
    if (err.message === "Debounced call superseded") {
      console.log("Request for 'ab' was superseded");
    }
  }
  
  try {
    const results = await debouncedFetch("abc");
    console.log("Results for 'abc':", results);
  } catch (err) {
    console.error("Error for 'abc':", err);
  }
}

simulateUserInput();

// 输出:
// (500ms后)
// Fetching data for: abc
// Request for 'a' was superseded
// Request for 'ab' was superseded
// Results for 'abc': ["result1 for abc", "result2 for abc"]

应用场景

  1. 搜索建议

    • 用户输入时实时获取搜索建议
    • 避免每次按键都发送请求
  2. 表单自动保存

    • 用户编辑表单时自动保存
    • 避免频繁保存操作
  3. API调用优化

    • 防止重复提交
    • 减少不必要的API调用
  4. 实时数据过滤

    • 用户调整过滤器时更新数据
    • 避免频繁重渲染