缓存是一个重要的概念,在面对非常耗时的计算的时候,通过缓存可以有效提高程序的运行性能。对于同步函数来说,缓存其运行结果非常简单。但是对于异步函数来说,我们需要做一些简单的设计。
如何缓存
我们先来介绍下缓存的概念和基本设计。假设我们有一个计算平方数的函数:
function getSquare(x){
return x * x;
}
2
3
要缓存这个函数,我们可以这么实现:
const memo = {};
function getSquare(x){
if(memo.hasOwnProperty(x)) {
return memo[x];
}
memo[x] = x * x;
return memo[x];
}
2
3
4
5
6
7
8
9
整理下代码,我们创建一个 memoize
工具函数,这个函数有两个参数,一个是要被缓存的函数,一个是 key
生成函数(每个缓存结果在缓存中应该有一个独一无二的 key)。
注意我们的 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];
}
}
2
3
4
5
6
7
8
9
10
11
12
这时候,上面计算平方数的函数可以这么缓存:
const memoGetSquare = memoize(getSquare, num => num);
缓存一个多参数函数:
const getDivision = (a, b) => a / b;
const memoGetDivision= memoize(getDivision, (a, b) => `${a}_${b}`);
2
3
4
缓存异步函数
上面介绍了缓存的概念和基本设计,并展示了如何缓存同步函数。那么如何对异步函数进行缓存呢?
假设我们有一个非常耗时的函数 expensiveOperation(key)
,该函数有一个回调参数,在函数运行结束后,通过回调函数返回结果。
expensiveOperation(key, ( data) => {
// Do something
})
2
3
与上面的缓存方法类似,我们可以先这么设计:
const memo = {}
function memoExpensiveOperation(key, callback){
if(memo.hasOwnProperty(key)){
callback(memo[key]);
return;
}
expensiveOperation(key, data => {
memo[key] = data;
callback(data);
});
}
2
3
4
5
6
7
8
9
10
11
12
13
非常简单直接,但是有一个问题。假设我们先调用了一次这个函数,在函数返回之前,又用相同的参数再调用一次。这个时候,因为函数是异步的,缓存起不到作用。
我们需要加一个进行中的缓存。
const memo = {}, progressQueues = {};
function memoExpensiveOperation(key, callback){
if(memo.hasOwnProperty(key)){
callback(memo[key]);
return;
}
// 在 progressQueues 记录一个进行中的调用
if(!progressQueues.hasOwnProperty(key)){
progressQueues[key] = [callback];
} else {
progressQueues[key].push(callback);
return;
}
expensiveOperation(key, (data) => {
// 缓存结果
memo[key] = data;
// 将进行中队列中的会有回调调用一遍
for(let callback of progressQueues[key]) {
callback(data);
}
// 清楚进行中的回调
delete progressQueue[key];
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
我们做一下简单封装:
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(memo[key]);
return;
}
if( !progressQueues.hasOwnProperty(key) ){
progressQueues[key] = [callback];
} else {
progressQueues[key].push(callback);
return;
}
fn.call(this, ...args , (data) => {
memo[key] = data;
for(let callback of progressQueues[key]) {
callback(data);
}
delete progressQueue[key];
});
}
}
// 使用样例
const memoExpensiveOperation = memoizeAsync(expensiveOperation, key => key);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
缓存 promise
假设我们有一个函数 processData(key)
,返回一个 promise。
缓存 promise 本身与缓存一个同步函数相同,这里不再赘述。
如果我们要缓存 promise 返回的值呢?其实与缓存异步函数类似,我们可以这么设计:
const memo = {}, progressQueues = {};
function memoProcessData(key){
return new Promise((resolve, reject) => {
// 如果已经缓存,直接 resolve
if(memo.hasOwnProperty(key)){
resolve(memo[key]);
return;
}
// 缓存进行中的状态
if( !progressQueues.hasOwnProperty(key) ){
progressQueues[key] = [[resolve, reject]];
} else {
progressQueues[key].push([resolve, reject]);
return;
}
processData(key)
.then(data => {
memo[key] = data; // 缓存结果
// 处理所有进行中的
for(let [resolver, ] of progressQueues[key]) {
resolver(data);
}
})
.catch(error => {
// 失败了,处理所有进行中的回调
for(let [, rejector] of progressQueues[key]) {
rejector(error);
}
})
.finally(() => {
// 清理进行中的回调
delete progressQueues[key];
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
控制缓存的规模
上面介绍的缓存方法中,我们没有控制缓存规模。也就是说,每用一个不同的 key 调用一次缓存的函数,都会记录一次缓存结果。慢慢的缓存将会越来越大,最终可能导致严重的后果。
我们需要对缓存的规模进行简单的控制,我们可以使用一些缓存驱逐策略来处理这样的问题。比如 LRU(Least Recently Used)open in new window。
关注微信公众号,获取最新推送~
加微信,深入交流~