记忆函数的一些思考


前言

我在 React 中中处理 Promise 依赖执行时时长遇到这样的场景,A、B、C 三个函数,但是 B 和 C 依赖 A 函数执行的结果执行,并且 B 和 C 的执行很可能在两个完全不着边际的组件中,没有联系。同时 A 函数有可能存在大性能消耗或者大资源请求的请求的情况,那么重复的请求就浪费一定资源,也有可能会造成多次系统的等待挂起。

B、C 也许没有先后顺序,串行并行都有可能,那么从 B、C 或者更多依赖 A 函数计算的函数就不好一一去进行优化处理。最好的方式就是对 A 进行缓存。

这样的场景在开发时大多存在于请求的依赖或计算的依赖。

使用记忆函数进行处理

对这些重复且不那么依靠计算新鲜度的函数,我想到的方法就是缓存,函数计算的缓存那就是记忆函数。

它的实现很简单,这是一个简单的设计:

/**
 * 记忆函数
 *
 * @param targetFunction 需要进行计算缓存的函数
 * @param createMemoryID 获取记忆键的函数
 * @returns 一个 targetFunction 的高阶记忆函数,附加 historyExec 对记忆进行控制
 */
export function memoryFunction<P extends Array<unknown>, R>(
  targetFunction: (...p: P) => R,
  createMemoryID: (args: P) => any = JSON.stringify
) {
  const memoryMap = new Map();

  function HOFTargetFunction(...p: P): R {
    const MemoryID = createMemoryID(p);

    if (memoryMap.has(MemoryID)) {
      return memoryMap.get(MemoryID);
    }

    const result = targetFunction(...p);
    memoryMap.set(MemoryID, result);

    return result;
  }

  return Object.assign(HOFTargetFunction, { memoryMap });
}

上面函数中,我们对目标函数的执行结果进行保存,通过 createMemoryID 系列化函数参数生成这次执行的缓存标识,以记录缓存的位置。

缓存标识进行序列化是最好的,因为参数数组总是新的,没办法很好的通过引用值来进行缓存记录,除非固定参数数量。

这只是个简单的雏形,但是已经足够使用了。

简单使用

一个简单的例子:

// 用例函数
const foo = (index: number) => {
  console.log(`index is ${index}`);
  return index * 2;
};

// 用例函数的记忆函数
const fooMemo = memoryFunction(foo);

fooMemo(10); // 20
fooMemo(10); // 20

// 仅输出一次 "index is 10"

访问异步函数并清理缓存:

const foo = () => new Promise((resolve) => setTimeout(() => resolve(Math.random()), 500));
const fooMemo = memoryFunction(foo);

// 打印结果记作 R1
fooMemo().then(console.log);

setTimeout(() => {
  // 此时多次打印结果和 R1 相同
  fooMemo().then(console.log);
  fooMemo().then(console.log);

  // 清除记忆后再次执行,结果和 R1 不同
  fooMemo.memoryMap.clear();
  fooMemo().then(console.log);
}, 2000);

你不必担心清理缓存对旧执行的影响,因为没有缓存时总是返回当前执行的结果:

const foo = () => new Promise((resolve) => setTimeout(() => resolve(Math.random()), 500));
const fooMemo = memoryFunction(foo);

// 因为每次执行清理了缓存,所以每次的执行结果总是新鲜的
for (let i = 0; i < 10; i++) {
  fooMemo().then(console.log);
  fooMemo.memoryMap.clear();
}

// 打印结果类似:
// 0.554479766551786
// 0.5500302592982389
// 0.7122983594409036
// ...