从请求缓存函数深入理解JavaScript闭包

71 阅读2分钟

前言

在日常前端开发中,我们经常会遇到需要缓存请求的场景,比如解决重复请求、为什么不从代码优化呢,比如一个页面多个地方调用,你如果改,怎么改?父组件请求,一层一层传?首先是层级太多,其次是容易出bug。今天,我们就通过一个请求缓存函数的实现,来解决问题和深入理解JavaScript中的闭包及其应用。

闭包基础

首先,让我们简单回顾一下闭包的概念:

function outer() {
  let count = 0;
  return function inner() {
    count++;
    return count;
  };
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2

在这个例子中,inner函数能够访问outer函数的count变量,这就是闭包的基本表现。

请求缓存函数实现

现在,让我们来看一个实际的请求缓存函数实现,它充分利用了闭包的特性:

const createMemoReq = () => {
  // 闭包内的缓存存储
  const pendingRequests = new Map();

  // 缓存项接口定义
  const createCacheItem = (promise, cacheTimeMS) => ({
    promise,
    cacheTimeMS,
    responseTimeMs: Infinity
  });

  return function memoReq(fn, cacheTimeMS = 0, context) {
    return function (...args) {
      // 生成包含上下文的缓存键
      const key = `${fn.name}|${context ? JSON.stringify(context) : 'null'}|${JSON.stringify(args)}`;

      // 检查缓存有效性
      const checkCacheValid = () => {
        const cache = pendingRequests.get(key);
        if (!cache) return false;
        const isExpired = (Date.now() - cache.responseTimeMs) >= cache.cacheTimeMS;
        if (isExpired) pendingRequests.delete(key);
        return !isExpired && cache;
      };

      // 返回有效缓存
      if (checkCacheValid()) {
        return pendingRequests.get(key).promise;
      }

      // 创建新请求
      const executeRequest = () => {
        const promise = fn.apply(context || this, args)
          .then(response => {
            pendingRequests.set(key, {
              ...pendingRequests.get(key),
              responseTimeMs: Date.now()
            });
            return response;
          })
          .catch(error => {
            pendingRequests.delete(key);
            throw error;
          });

        pendingRequests.set(key, createCacheItem(promise, cacheTimeMS));
        return promise;
      };

      return executeRequest();
    };
  };
};

闭包在实现中的应用

在这个实现中,我们充分利用了闭包的几个特性:

  1. 数据私有化

    • pendingRequests Map被封闭在createMemoReq的作用域内
    • 外部无法直接访问或修改缓存数据
  2. 状态保持

    • 每次调用memoReq返回的函数都能访问同一个pendingRequests
    • 缓存状态在多次调用间得以保持
  3. 函数工厂

    • createMemoReq是一个工厂函数,可以创建多个独立的缓存实例

使用示例

使用前

export function getListFromRedis() {
  return axios.get("/admin-api/stock/symbol/list-all");
}

使用后

export const getListFromRedis = memoReq( function getListFromRedis() {
  return axios.get("/admin-api/stock/symbol/list-all");
}, 1000)

使用效果

// 第一次调用 - 发起实际请求
getListFromRedis().then(console.log); 

// 3秒内再次调用 - 返回缓存结果
getListFromRedis().then(console.log);

// 3秒后调用 - 发起新请求
setTimeout(() => {
  getListFromRedis().then(console.log);
}, 3001);

总结与展望

通过这个例子,我们看到了闭包在实际开发中的强大能力。这种实现方式简单有效,但仍有优化空间:

  1. 可以添加缓存大小限制
  2. 实现更复杂的缓存策略(如LRU)
  3. 使用更加便捷的装饰器模式尽可能小的改造

下期预告:总所周知这样使用会报错,下期我将分享如何通过Babel插件,用装饰器语法实现更优雅的请求缓存,就像这样:

@memoReq(3000)
async function getListFromRedis() {
  return axios.get("/admin-api/stock-symbol/list-all");
}