重复请求优化

设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:两个组件必须有一个共同的祖先组件,如果这两个组件是同级的兄弟组件倒也还好,如果非同级,那么数据的传参就会有些麻烦了。那么还有其他办法吗?当然是有的。

我们可以换个思路,每个组件还是保持原有的业务逻辑不变,从请求接口处做文章。既然是同一个接口调用了两次,而且还是返回了相同的请求结果,那么不妨在第一次时调用成功时,就把请求结果缓存起来,等到第二次再调用时,直接返回缓存的数据。按照这个思路可以写出第一版的代码(这里用了 TS 方便查看参数的类型):

ts 复制代码
/**
 * 缓存请求的响应结果
 * 把发起请求的 Promise 对象挂载在原型对象上
 * @param request 请求函数
 */
function cacheRequest<T>(request: (...args: any[]) => Promise<T>) {
  const cache = Symbol("cache")
  return function (...args: any[]): Promise<T> {
    if (!request.prototype[cache]) {
      request.prototype[cache] = request(...args)
    }
    return request.prototype[cache]
  }
}
  • 首先 cacheRequest 函数,需要接收一个参数 request,request 是一个返回结果为 Promise 对象的函数。cacheRequest 执行完后返回一个新的匿名函数。
  • 然后,在匿名函数的内部,先判断 request 的原型对象上是否有 cache(这里的 cache 使用了 Symbol 类型,确保键名唯一)。也即,是否有缓存过的请求结果,如果没有,说明是第一次调用,则将 request 的执行结果存到缓存里。如果有缓存,则直接返回缓存。
  • 可以看到,缓存也是一个 Promise 类型。在同时调用多次请求时,只要在第一次调用执行后,已经把 Promise 存到缓存里了,后续的请求返回的也是缓存里的 Promise,从而保证多个请求都指向同一个 Promise ,也即只会调用一次 API 接口。
  • 这里需要注意一点,由于需要往 request 的原型对象上挂载缓存,所以 request 不能是箭头函数。因为箭头函数没有 this,也就意味着没有原型对象。

小测一下:

js 复制代码
function cacheRequest(request) {
  const cache = Symbol("cache")
  return function (...args) {
    if (!request.prototype[cache]) {
      request.prototype[cache] = request(...args)
    }
    return request.prototype[cache]
  }
}

const request = function () {
  return new Promise(resolve => {
    console.log("fetch request")
    setTimeout(resolve, 2000)
  })
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
newRequest()

可以看到虽然 newRequest 调用了三次,但是 fetch request 只打印了一次,也就是说 request 只调用了一次,符合预期!但是,最后一次 newRequest 的调用,是在 3 秒后调用的,也是走的缓存,没有重新执行。仔细思考一下,后续无论什么时候调用 newRequest 都会使用缓存里的数据,不会重新调用请求了,这显然是不合理的。我们还需要加个缓存的过期时间,超过这个时间,就重新发起新的请求。第二版如下:

ts 复制代码
/**
 * 缓存请求的响应结果
 * 把发起请求的 Promise 对象挂载在原型对象上
 * 保证在 cacheTime 时间间隔内的多次请求,只会调用一次
 * @param request 请求函数
 * @param cacheTime 最大缓存时间(单位毫秒)
 */
export function cacheRequest<T>(request: (...args: any[]) => Promise<T>, cacheTime = 1000) {
  const cache = Symbol("cache")
  const lastTime = Symbol("lastTime")
  return function (...args: any[]): Promise<T> {
    if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
      request.prototype[cache] = request(...args)
      request.prototype[lastTime] = Date.now()
    }
    return request.prototype[cache]
  }
}
  • 首先,cacheRequest 新增一个入参 cacheTime,用于设置过期时间,默认为 1 秒。
  • 其次,在原型对象上新增了一个 lastTime 属性,用来记录最后一次调用的时间。
  • 当缓存为空,或者当前时间距离上一次调用时间超过缓存过期时间时,更新 cache 和 lastTime 。

再来小测一下:

js 复制代码
function cacheRequest(request) {
  const cache = Symbol("cache")
  const lastTime = Symbol("lastTime")
  return function (...args) {
    if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
      request.prototype[cache] = request(...args)
      request.prototype[lastTime] = Date.now()
    }
    return request.prototype[cache]
  }
}

const request = function () {
  return new Promise(resolve => {
    console.log("fetch request")
    setTimeout(resolve, 2000)
  })
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
setTimeout(newRequest, 3000)

这一次,fetch request 打印了两次,符合预期,完美!

相关推荐
panxianren5 小时前
Bun 1.4 的 bun test --parallel 将 4 秒的测试套件缩短到约 1 秒
java·javascript
可乐鸡翅yeah_8 小时前
FFmpeg 生成 HLS 独立分片 independent‑segments 参数到底有什么用
javascript·ffmpeg·音视频·safari·m3u8
福兮说8 小时前
canvas 旋转图片的六个坑:四角被裁、Math.ceil 多出 1px 黑边、JPG 角发黑、翻转方向反了
前端·javascript·图像处理·canvas
Ai-_Man10 小时前
您您这可以把Grok的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。AI导出鸭
javascript·人工智能·ai·小程序·电脑
liangshanbo121510 小时前
JavaScript 异步核心主线
javascript·事件循环·异步操作
传人once11 小时前
悬停旋转放大和位移效果如何写
javascript·css·动画
默_笙13 小时前
🍔 中间件不只是打日志:四个钩子、一次短路,和自带的工具
前端·javascript
Dovis(誓平步青云)14 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
xieter14 小时前
【技术精选】TypeScript 高级类型体操:条件类型与模板字面量深度剖析 (2026-10-06)
javascript
ss27315 小时前
AI全栈实战 | 3.3-02 Python 并发:有 GIL 为什么还用多线程,asyncio 和 JS 事件循环同源不同味
开发语言·javascript·python