JavaScript 中的记忆化

2025-05-25

JavaScript 中的记忆化

注意:本文最初发表于StackFull.dev

记忆化是一个很有用的概念。它有助于避免耗时或昂贵的计算,只需执行一次即可。将记忆化应用于同步函数相对简单。本文旨在介绍记忆化背后的总体概念,然后深入探讨在尝试记忆异步函数时遇到的问题及其解决方案。

记忆化

让我们从记忆纯函数开始。假设我们有一个名为的函数getSquare,它返回给定值的平方:

  function getSquare(x){
     return x * x
  }

Enter fullscreen mode Exit fullscreen mode

为了记住这一点,我们可以做这样的事情:


const memo = {}

function getSquare(x){
    if(memo.hasOwnProperty(x)) {
      return memo[x]
    }
    memo[x] = x * x
    return memo[x]
}

Enter fullscreen mode Exit fullscreen mode

因此,我们用几行代码就记住了我们的getSquare功能。

让我们创建一个memoize助手。它接受一个纯函数作为第一个参数,并接受一个getKey函数(该函数根据给定的参数返回一个唯一键)作为第二个参数,以返回该函数的记忆版本:

function memoize(fn, getKey){
  const memo = {}
  return function memoized(...args){
     const key = getKey(...args)
     if(memo.hasOwnProperty(key)) return memo[key]

     memo[key] = fn.apply(this, args)
     return memo[key]
  }
}
Enter fullscreen mode Exit fullscreen mode

我们可以将此函数应用getSquare如下:

const memoGetSquare = memoize(getSquare, num => num) 
Enter fullscreen mode Exit fullscreen mode

记忆接受多个参数的函数:

const getDivision = (a, b) => a/b

// memoizing using the helper
const memoGetDivision= memoize(getDivision, (a, b) => `${a}_${b}`)
Enter fullscreen mode Exit fullscreen mode

记忆异步函数

假设有一个名为的函数expensiveOperation(key),它接受一个键作为参数,并在通过回调返回最终结果之前执行一些异步操作:

// does some async operation and invokes the callback with final result

expensiveOperation(key, ( data) => {
   // Do something
})
Enter fullscreen mode Exit fullscreen mode

让我们使用与上面类似的概念来记忆这个函数:

const memo = {}

function memoExpensiveOperation(key, callback){
  if(memo.hasOwnProperty(key)){
    callback(memo[key])
    return
  } 

  expensiveOperation(key, data => {
   memo[key] = data
   callback(data)
  })
}
Enter fullscreen mode Exit fullscreen mode

这很简单。但是等等!这还不能解决所有问题。考虑以下场景:

1-expensiveOperation使用键“a”调用

2- 当 #1 仍在进行时,使用相同的键再次调用它

该函数会针对同一操作运行两次,因为 #1 尚未将最终数据保存到 中memo。这并非我们想要的效果。我们希望并发调用能够在最早的调用完成后立即得到解决。让我们看看如何实现这一点:

const memo = {}, progressQueues = {}

function memoExpensiveOperation(key, callback){
     if(memo.hasOwnProperty(key)){
       callback(memo[key])
       return
      }

     if(!progressQueues.hasOwnProperty(key)){
        // processing new key, create an entry for it in progressQueues
        progressQueues[key] = [callback]

      } else {
       // processing a key that's already being processed, enqueue it's callback and exit. 
        progressQueues[key].push(callback);
        return
      }

      expensiveOperation(key, (data) => {
           // memoize result
           memo[key] = data 
           // process all the enqueued items after it's done
           for(let callback of progressQueues[key]) {
                callback(data)
           }
           // clean up progressQueues
           delete progressQueue[key]
       })

}
Enter fullscreen mode Exit fullscreen mode

我们可以更进一步,就像上一节一样,创建一个可重复使用的助手memoizeAsync

function memoizeAsync(fn, getKey){
   const memo = {}, progressQueues = {}

   return function memoized(...allArgs){
       const callback = allArgs[allArgs.length-1]
       const args = allArgs.slice(0, -1)
       const key = getKey(...args)

        if(memo.hasOwnProperty(key)){
            callback(key)
            return
        }


        if( !progressQueues.hasOwnProperty(key) ){
           // processing new key, create an entry for it in progressQueues
           progressQueues[key] = [callback]
        } else {
           // processing a key that's already being processed, enqueue it's callback and exit. 
           progressQueues[key].push(callback);
           return
        }

        fn.call(this, ...args , (data) => {
           // memoize result
           memo[key] = data 
           // process all the enqueued items after it's done
           for(let callback of progressQueues[key]) {
                callback(data)
           }
           // clean up progressQueues
           delete progressQueue[key]
       })
   }
}

// USAGE

const memoExpensiveOperation = memoizeAsync(expensiveOperation, key => key)

Enter fullscreen mode Exit fullscreen mode

承诺

假设我们有一个函数processData(key),它接受一个键作为参数并返回一个 Promise。让我们看看如何将它记忆化。

记住底层的承诺:

最简单的方法是记住针对该密钥发出的承诺。如下所示:

const memo = {}
function memoProcessData(key){
  if(memo.hasOwnProperty(key)) {
    return memo[key]
  }

  memo[key] = processData(key) // memoize the promise for key
  return memo[key]
}
Enter fullscreen mode Exit fullscreen mode

这段代码相当简单,不言自明。实际上,我们可以使用memoize之前创建的辅助函数:

 const memoProcessData = memoize(processData, key => key)
Enter fullscreen mode Exit fullscreen mode

我们可以记住 Promise 返回的值吗?

是的。我们可以在这里应用与回调相同的方法。尽管为了记住这样的函数,这样做可能有点过头了:

  const memo = {},  progressQueues = {}

  function memoProcessData(key){

    return new Promise((resolve, reject) => {
      // if the operation has already been done before, simply resolve with that data and exit
      if(memo.hasOwnProperty(key)){
        resolve(memo[key])
        return;
      }

      if( !progressQueues.hasOwnProperty(key) ){
        // called for a new key, create an entry for it in progressQueues
        progressQueues[key] = [[resolve, reject]]

      } else {
       // called for a key that's still being processed, enqueue it's handlers and exit.         
        progressQueues[key].push([resolve, reject]);
        return;
      }


      processData(key)
        .then(data => {
            memo[key] = data; // memoize the returned data
            // process all the enqueued entries after successful operation
            for(let [resolver, ] of progressQueues[key])
              resolver(data)
        })
        .catch(error => {
           // process all the enqueued entries after failed operation
           for(let [, rejector] of progressQueues[key])
              rejector(error);
         })
        .finally(() => {
          // clean up progressQueues
           delete progressQueues[key]
         })
    })
  }
Enter fullscreen mode Exit fullscreen mode

进一步改进

由于我们使用一个memo对象来跟踪已记忆的操作,如果对expensiveOperation各种键的调用次数过多(并且每个操作在处理后都会返回大量数据),该对象的大小可能会超出理想值。为了处理这种情况,我们可以使用缓存驱逐策略,例如LRU(最近最少使用)。它可以确保我们在记忆操作时不会超出内存限制!


本文最初发表于StackFull.dev。如果您喜欢阅读本文,不妨订阅我的新闻通讯。这样,每当我发表新想法时,我都会及时与您联系!

文章来源:https://dev.to/anishkumar/memoizing-fetch-api-calls-in-javascript-1d16
PREV
Webpack 5:初学者指南
NEXT
Taiga UI:开源一年 开源 有什么新东西?即将推出什么?