缓存是一个重要的概念,在面对非常耗时的计算的时候,通过缓存可以有效提高程序的运行性能。对于同步函数来说,缓存其运行结果非常简单。但是对于异步函数来说,我们需要做一些简单的设计。

如何缓存

我们先来介绍下缓存的概念和基本设计。假设我们有一个计算平方数的函数:

function getSquare(x){
    return x * x;
}
1
2
3

要缓存这个函数,我们可以这么实现:

const memo = {};

function getSquare(x){
    if(memo.hasOwnProperty(x)) {
        return memo[x];
    }
    memo[x] = x * x;
    return memo[x];
}
1
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];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

这时候,上面计算平方数的函数可以这么缓存:

const memoGetSquare = memoize(getSquare, num => num);
1

缓存一个多参数函数:

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

const memoGetDivision= memoize(getDivision, (a, b) => `${a}_${b}`);

1
2
3
4

缓存异步函数

上面介绍了缓存的概念和基本设计,并展示了如何缓存同步函数。那么如何对异步函数进行缓存呢?

假设我们有一个非常耗时的函数 expensiveOperation(key),该函数有一个回调参数,在函数运行结束后,通过回调函数返回结果。

expensiveOperation(key, ( data) => {
   // Do something
})
1
2
3

与上面的缓存方法类似,我们可以先这么设计:

const memo = {}

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

    expensiveOperation(key, data => {
        memo[key] = data;
        callback(data);
    });
}
1
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];
    });
}
1
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);
1
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];
        })
    })
}

1
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

关注微信公众号,获取最新推送~

加微信,深入交流~