Promise缓存/取消(可缓存的Promise/可取消的Promise)

79 阅读3分钟

typing.d.ts

/**
 * 自定义构建唯一标识的方法
 */
export type KeyBuilderFun<P extends unknown[]> = (...args: P) => string
export type Service<R, P extends unknown[]> = (...args: P) => Promise<R>

ShareablePromise.ts

import { KeyBuilderFun, Service } from './typing'

/**
 * 构建可共享Pending状态Promise结果的异步方法
 * @param fn await修饰的异步方法,或返回Promise结果的异步方法
 * @param keyBuilder 自定义构建可共享pending状态promise的唯一标识方法,或直接指定唯一标识
 * @returns 被包装之后的fn方法,此包装方法支持自动共享pending状态的promise
 */
export function buildShareablePendingPromiseMethod<R, P extends unknown[]>(
  fn: Service<R, P>,
  keyBuilder: KeyBuilderFun<P> | string
): Service<R, P> {
  let lastPromise: R | undefined
  let lastKey: string | undefined
  function doRun(key: string, ...args: P) {
    lastKey = key
    // @ts-ignore
    const retPromise = fn.apply<unknown, P, R>(this, args).finally(() => {
      if (lastKey === key) {
        // 如果当前key和lastKey是同一个,则清空
        lastKey = undefined
        lastPromise = undefined
      }
    })
    //@ts-ignore
    lastPromise = retPromise
    return retPromise
  }
  return function (...args: P) {
    const tmpLastPromise = lastPromise
    const key =
      typeof keyBuilder === 'string' ? keyBuilder : keyBuilder(...args)
    if (lastKey === key && tmpLastPromise) {
      console.log('使用 pending 缓存', key, lastKey, lastPromise)
      // 当前key与上一个key一致,且存在pending中的promise,则直接返回该pending中的promise
      return tmpLastPromise
    } else {
      // 没有与当前key相同的pending中的promise,则直接执行
      return doRun(key, ...args)
    }
  }
}

CancelablePromise.ts

import { KeyBuilderFun, Service } from './typing'
export class CancelPendingPromiseError extends Error {
  constructor(msg?: string) {
    super(msg)
  }
}
/**
 * 构建可取消promise
 * @param p 原始的promise
 * @returns
 */
export function buildCancelablePromise<T>(p: Promise<T>) {
  // 超时取消函数
  let outReject: undefined | ((reason?: unknown) => void)
  const rejectPromise = new Promise<void>((_, reject) => {
    outReject = reject
  })
  /**
   * 取消promise(实际是触发promise的reject回调)
   * @param reason 取消原因。与promise的reject回调方法的入参类型一致
   */
  function cancel(reason: string = '取消pending Promise') {
    if (outReject) outReject(new CancelPendingPromiseError(reason))
    else throw 'promise的reject方法未初始化'
  }
  const cancelablePromise = Promise.race([p, rejectPromise])
  return {
    /**
     * 可取消的promise
     */
    cancelablePromise,
    /**
     * 取消promise(实际是触发promise的reject回调)
     * @param reason 取消原因。与promise的reject回调方法的入参类型一致
     */
    cancel,
  }
}

/**
 * 构建可自动取消的异步方法
 * @param fn await修饰的异步方法,或返回Promise结果的异步方法
 * @param keyBuilder 自定义构建可取消promise的唯一标识方法,或直接指定唯一标识
 * @returns 被包装之后的fn方法,此包装方法支持自动取消
 */
export function buildCancelablePromiseMethod<R, P extends unknown[]>(
  fn: Service<R, P>,
  keyBuilder: KeyBuilderFun<P> | string
): Service<R, P> {
  let executing = false
  let lastKey: string | undefined
  let lastCancel: undefined | ((reason?: string) => void)
  function doRun(key: string, ...args: P) {
    executing = true
    lastKey = key
    const retPromise = fn
      // @ts-ignore
      .apply<unknown, P, Promise<R>>(this, args)
      .finally(() => {
        executing = false
        lastCancel = undefined
      })
    const { cancelablePromise, cancel } = buildCancelablePromise(retPromise)
    lastCancel = cancel
    return cancelablePromise as Promise<R>
  }
  return function (...args: P) {
    const key =
      typeof keyBuilder === 'string' ? keyBuilder : keyBuilder(...args)
    console.log('key对应promise状态:', key, executing, lastKey)
    if (!executing) {
      // 发起新请求
      return doRun(key, ...args)
    } else {
      const tmpLastCancel = lastCancel
      if (lastKey !== key && tmpLastCancel) {
        // 取消执行中
        console.log(`取消promise:${lastKey}`)
        // 这里不会中断程序运行,后面的doRun还是能够执行,只会会使得上一个调用的地方,出现CancelPendingPromiseError异常。如果UI组件未捕获该异常的话,会导致界面出错
        tmpLastCancel(`取消promise:${lastKey}`)
      }

      // 发起新请求
      return doRun(key, ...args)
    }
  }
}

CacheHelper.ts

interface CacheItem {
  value: any; // 缓存的值,可以根据实际类型替换为具体类型
  expireTime: number; // 过期时间戳
}

export class CacheHelper {
  private store: { [key: string]: CacheItem } = {};

  /**
   * 获取缓存
   * @param key 缓存键
   * @returns 缓存值,如果不存在或已过期则返回 null
   */
  getCache(key: string): any | null {
    const cache = this.store[key];
    if (!cache) {
      return null;
    }
    const curTime = new Date().getTime();
    if (curTime > cache.expireTime) {
      // 删除已缓存的数据
      this.removeCache(key);
      return null;
    }
    return cache.value;
  }

  /**
   * 删除缓存
   * @param key 缓存键
   */
  removeCache(key: string): void {
    delete this.store[key];
  }

  /**
   * 设置缓存
   * @param key 缓存键
   * @param value 缓存值
   * @param expireTime 过期时间(毫秒),默认 5 分钟
   */
  setCache(key: string, value: any, expireTime: number = 1000 * 60 * 5): void {
    this.store[key] = {
      value,
      expireTime: new Date().getTime() + expireTime,
    };
  }

  /**
   * 全局缓存包装器(支持异步函数)
   * @param fn 需要缓存的函数
   * @param keyBuilder 缓存键生成器(可以是字符串或函数)
   * @param time 缓存过期时间(毫秒),默认 5 分钟
   * @returns 包装后的函数
   */
  promiseGlobalCacheWrapper(
    fn: (...args: any[]) => Promise<any>,
    keyBuilder: string | ((...args: any[]) => string),
    time: number = 1000 * 60 * 5
  ): (...args: any[]) => Promise<any> {
    return async (...args: any[]): Promise<any> => {
      const key = typeof keyBuilder === 'string' ? keyBuilder : keyBuilder(...args);
      const cache = this.getCache(key);
      if (cache) {
        return cache;
      }
      const value = await fn(...args);
      if (value) {
        this.setCache(key, value, time);
      }
      return value;
    };
  }
}