只使用 Closure + IIFE 就能實作出 Cache

若 API 每次傳入的 Query String 相同,所回傳的結果也相同,同時也沒對資料庫做任何異動,對於這類 API,其實可以加上 Cache,當參數相同時,就不再向後端打 API,如此不但節省後端資源,前端的反應也更為迅速,使用者體驗更佳。

事實上 ECMAScript 的 Closure + IIFE 就能實作出 Cache,並不需要依賴其他 Package。

Version


Vue 2.5.22
Vue CLI 3.4.0
Axios 0.18.0

Component


components/HelloWorld.vue

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
<template>
<div>
ID:<input type="text" v-model="id"/>&nbsp;
<button @click="onSubmit">Submit</button>
<p></p>
{{ title }}
</div>
</template>

<script>
import fetchBook from '@/api/book-info';

const onSubmit = function() {
fetchBook({ id: this.id })
.then(res => this.title = res.data[0].title);
};

export default {
name: 'HelloWorld',
data: () => ({
id: null,
title: '',
}),
methods: {
onSubmit,
},
};
</script>

13 行

1
2
3
4
const onSubmit = function() {
fetchBook({ id: this.id })
.then(res => this.title = res.data[0].title);
};

當使用 GET query string 方式時,Axios 要求我們傳入一個 object,因此組了 { id: this.id },由於 fetchBook() 會回傳 Promise,因此使用 then() 方式寫入 title data。

API Function


api/book-info.js

1
2
3
4
5
6
import axios from 'axios';

const API = process.env.VUE_APP_API;
const URI = process.env.VUE_APP_BOOK_INFO;

export default args => axios.get(`${API}${URI}`, { params: args });

API 部分將傳入的 args object,再組一個 key 為 params 的 object 傳入 Axios.get()

這是最普通使用 GET query string 方式,每次呼叫都會重打一次 API。

若 API 具有 Pure Function 特性,也就是每次呼叫時,只要 Query String 相同,結果就相同,且資料庫也不會有所異動 (無 Side Effect),此時就可以將 API function 加以 cache,如此將大幅增加前後端執行效率。

Cacheable


api/book-info.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import axios from 'axios';

const API = process.env.VUE_APP_API;
const URI = process.env.VUE_APP_BOOK_INFO;

const toKey = args => Object.values(args).join('_');
const fn = args => axios.get(`${API}${URI}`, { params: args });

export default (() => {
const storage = {};

return args => {
const key = toKey(args);
return storage[key] ? storage[key] : storage[key] = fn(args);
};
})();

要實作 cache 其實並不需要額外 package,只要使用 ECMAScript 語言特性:Closure + IIFE 即可達成。

第 7 行

1
const fn = args => axios.get(`${API}${URI}`, { params: args });

將原本的 args => axios.get() 另外定義成 fn()

第 9 行

1
2
3
4
5
6
export default (() => {
...
return args => {
...
};
})();

原本是 export function,現在改成 export IIFE,且 IIFE 回傳參數為 args 的新 function。

10 行

1
const storage = {};

Cache 的關鍵就在此:在 IIFE 內宣告 storage object。

  • 因為 object 具有 key / value 特性,因此可當作 cache 使用
  • 由於 object 是宣告在 return function 外,也就是 Closure,所以無論 function 被呼叫幾次,storage object 依然存在,因此可當作 cache 使用

第 6 行

1
const toKey = args => Object.values(args).join('_');

要能使用 cache,key 是關鍵因素,特別建立了 toKey() 產生 key。

由於 args 為 object,先使用 Object.values() 轉成 array,再使用 Array.join() 產生中間以 _ 分隔的 string 作為 key。

14 行

1
return storage[key] ? storage[key] : storage[key] = fn(args);

若該 key 存在於 storage object,則直接回傳 storage[key],內部存的是 Promise

若不存在,則呼叫 fn() 打 API,先存到 storage object 後再回傳。

當然也可以將 value 存進 storage object,由於新 function 回傳的是 Promise,若 cache 內找到 value,還必須自行 new Promise(),若找不到則還需 fn(args).then() 處理,比較麻煩

cache000

目前資料只有 ID 1 到 3,所以前三筆要打 API,之後再重複輸入相同 ID,就直接從 cache 取出了。

Higher Order Function


api/book-info.js

1
2
3
4
5
6
7
8
9
import axios from 'axios';
import { cache } from '../helpers/promise';

const API = process.env.VUE_APP_API;
const URI = process.env.VUE_APP_BOOK_INFO;

const fn = args => axios.get(`${API}${URI}`, { params: args });

export default cache(fn);

基本的機制都有了,剩下就是 reuse 問題,是否能將這種 cache 機制抽成 cache() Higher Order Function 呢 ? 將來任何 function 只要經過 cache() 包裝過,就變成 Cacheable Function。

helpers/promise.js

1
2
3
4
5
6
7
8
9
10
const toKey = args => Object.values(args).join('_');

export const cache = fn => (() => {
const storage = {};

return args => {
const key = toKey(args);
return storage[key] ? storage[key] : storage[key] = fn(args);
};
})();

toKey() 重構到 helpers/promise.js 內,且將原本在 api/book-info.js 的 code 抽成 cache() Higer Order Function。

由於 cache 的儲存與判斷機制都相同,只有 axios.get()fn() 不同,因此將 fn() 抽成 fn() parameter,由 api function 傳入。

如此任何 api function 只要經過 cache() 包裝過,就成了 Cacheable Function,大大提高開發效率。

Conclusion


  • Closure 天生就能夠 cache,只要善用 ECMAScript 語言特性就能實作 cache
  • 可將 Promise 直接存入 storage object,可避開自行建立 Promise
  • 對於 Code Reuse 部分,OOP 會將相同部分使用繼承,或 Extract Class 之後再 DI 注入;FP 則會將相同部分抽成 Higher Order Function,相異部分傳入 callback function

Sample Code


完整的範例可以在我的 GitHub 上找到

2019-02-04