詳解JavaScript 中的批處理和快取
場景
最近在生產環境遇到了下面這樣一個場景:
後臺在字典表中儲存了一些之前需要前後端共同維護的列舉值,並提供根據 type/id 獲取字典的 API。所以在渲染列表的時候,有很多列表的欄位直接就是字典的 id,而沒有經過後臺的資料拼裝。
起初,吾輩解決問題的流程如下
- 確定字典欄位,新增轉換後的物件型別介面
- 將物件列表進行轉換得到其中字典欄位的所有值
- 對字典 id 列表進行去重
- 根據 id 列表從後臺獲取到所有的字典資料
- 將獲得的字典資料轉換為 id => 字典 的 Map
- 遍歷最初的列表,對裡面指定的字典欄位進行轉換
可以看到,上面的步驟雖然不麻煩,但卻十分繁瑣,需要定義額外的型別不說,還很容易發生錯誤。
思路
- 使用 非同步批處理 + LRU 快取 優化效能
- 支援非同步 formatter 獲得更好的使用體驗
實現非同步批處理
參考實現:
import { wait } from '../async/wait' /** * 將多個併發非同步呼叫合併為一次批處理 * @param handle 批處理的函式 * @param ms 等待的時長(時間越長則可能合併的呼叫越多,否則將使用微任務只合並一次同步執行的所有呼叫) */ export function batch<P extends any[],R extends any>( handle: (list: P[]) => Promise<Map<P,R | Error>>,ms: number = 0,): (...args: P) => Promise<R> { //引數 => 結果 對映 const resultCache = new Map<string,R | Error>() //引數 => 次數的對映 const paramCache = new Map<string,number>() //當前是否被鎖定 let lock = false return async function (...args: P) { const key = JSON.stringify(args) paramCache.set(key,(paramCache.get(key) || 0) + 1) await Promise.all([wait(() => resultCache.has(key) || !lock),wait(ms)]) if (!resultCache.has(key)) { try { lock = true Array.from( await handle(Array.from(paramCache.keys()).map((v) => JSON.parse(v))),).forEach(([k,v]) => { resultCache.set(JSON.stringify(k),v) }) } finally { lock = false } } const value = resultCache.get(key)! paramCache.set(key,paramCache.get(key)! - 1) if ((paramCache.get(key) || 0) <= 0) { paramCache.delete(key) resultCache.delete(key) } if (value instanceof Error) { resultCache.delete(key) throw value } return value as R } }
實現批處理的基本思路如下
1.使用 Map paramCache 快取傳入的 引數 => 剩餘呼叫次數(該引數還需要查詢幾次結果)
2.使用 Map resultCache 快取 引數 => 結果
3.使用 lock 標識當前是否有函式正在執行
4.滿足以下條件需要等待
Map 中不包含結果
目前有其它呼叫在執行
還未滿最小等待時長(收集呼叫的最小時間片段)
5.使用 lock 標識正在執行
6.判斷是否已經存在結果
如果不存在則執行批處理處理當前所有的引數
7.從快取 Map 中獲取結果
8.將 paramCache 中對應引數的 剩餘呼叫次數 -1
9.判斷是否還需要保留該快取(該引數對應的剩餘呼叫次數為 0)
10.判斷快取的結果是否是 Error
是的話則 throw 丟擲錯誤
LRU 快取
參考: Wiki 快取演算法,實現 MemoryCache
問:這裡為什麼使用快取?
答:這裡的字典介面在大概率上是冪等的,所以可以使用快取提高效能
問:那麼快取策略為什麼要選擇 LRU 呢?
答:毫無疑問 FIFO 是不合理的
問:那為什麼不選擇 LFU 演算法呢?它似乎能保留訪問最頻繁的資源
答:因為字典表並非完全冪等,吾輩希望避免一種可能–訪問最多的字典一直沒有刪除,而它在資料庫已經被更新了。
大致實現思路如下
1.使用一個 Map 記錄 快取 key => 最後訪問時間
2.每次獲取快取時更新最後訪問時間
3.新增新的快取時檢查快取數量
如果超過最大數量,則刪除最後訪問時間距離現在最長的一個快取
4.新增新的快取
Pass: 不要吐槽效能很差啦,這個場景下不會快取特別多的元素啦,最多也就不到 1000 個吧
結合高階函式
現在,我們可以結合這兩種方式了,同時使用 onceOfSameParam/batch 兩個高階函式來優化 根據 id 獲取字典資訊 的 API 了。
const getById = onceOfSameParam( batch<[number],Dict>(async (idList) => { if (idList.length === 0) { return new Map() } // 一次批量處理多個 id const list = await this.getByIdList(uniqueBy(idList.flat())) return arrayToMap( list,(dict) => [dict.id],(dict) => dict,) },100),)
支援非同步 formatter
原本想要支援 ListTable 的非同步 formatter
函式,但後來想想,如果 slot
裡也包含字典 id 呢?那是否 slot
也要支援非同步呢?這可是個比較棘手的問題,所以還是不支援好了。
最終,吾輩在元件與 API 之間添加了 *Service 中間層負責處理資料轉換。
以上就是詳解JavaScript 中的批處理和快取的詳細內容,更多關於JavaScript 中的批處理和快取的資料請關注我們其它相關文章!