JavaScript 中的记忆化
注意:本文最初发表于StackFull.dev。
记忆化是一个很有用的概念。它有助于避免耗时或昂贵的计算,只需执行一次即可。将记忆化应用于同步函数相对简单。本文旨在介绍记忆化背后的总体概念,然后深入探讨在尝试记忆异步函数时遇到的问题及其解决方案。
记忆化
让我们从记忆纯函数开始。假设我们有一个名为的函数getSquare
,它返回给定值的平方:
function getSquare(x){
return x * x
}
为了记住这一点,我们可以做这样的事情:
const memo = {}
function getSquare(x){
if(memo.hasOwnProperty(x)) {
return memo[x]
}
memo[x] = x * x
return memo[x]
}
因此,我们用几行代码就记住了我们的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]
}
}
我们可以将此函数应用getSquare
如下:
const memoGetSquare = memoize(getSquare, num => num)
记忆接受多个参数的函数:
const getDivision = (a, b) => a/b
// memoizing using the helper
const memoGetDivision= memoize(getDivision, (a, b) => `${a}_${b}`)
记忆异步函数
假设有一个名为的函数expensiveOperation(key)
,它接受一个键作为参数,并在通过回调返回最终结果之前执行一些异步操作:
// does some async operation and invokes the callback with final result
expensiveOperation(key, ( data) => {
// Do something
})
让我们使用与上面类似的概念来记忆这个函数:
const memo = {}
function memoExpensiveOperation(key, callback){
if(memo.hasOwnProperty(key)){
callback(memo[key])
return
}
expensiveOperation(key, data => {
memo[key] = data
callback(data)
})
}
这很简单。但是等等!这还不能解决所有问题。考虑以下场景:
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]
})
}
我们可以更进一步,就像上一节一样,创建一个可重复使用的助手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)
承诺
假设我们有一个函数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]
}
这段代码相当简单,不言自明。实际上,我们可以使用memoize
之前创建的辅助函数:
const memoProcessData = memoize(processData, key => key)
我们可以记住 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]
})
})
}
进一步改进
由于我们使用一个memo
对象来跟踪已记忆的操作,如果对expensiveOperation
各种键的调用次数过多(并且每个操作在处理后都会返回大量数据),该对象的大小可能会超出理想值。为了处理这种情况,我们可以使用缓存驱逐策略,例如LRU(最近最少使用)。它可以确保我们在记忆操作时不会超出内存限制!
本文最初发表于StackFull.dev。如果您喜欢阅读本文,不妨订阅我的新闻通讯。这样,每当我发表新想法时,我都会及时与您联系!
文章来源:https://dev.to/anishkumar/memoizing-fetch-api-calls-in-javascript-1d16