1. 程式人生 > 程式設計 >詳解JavaScript 中的批處理和快取

詳解JavaScript 中的批處理和快取

場景

最近在生產環境遇到了下面這樣一個場景:
後臺在字典表中儲存了一些之前需要前後端共同維護的列舉值,並提供根據 type/id 獲取字典的 API。所以在渲染列表的時候,有很多列表的欄位直接就是字典的 id,而沒有經過後臺的資料拼裝。

起初,吾輩解決問題的流程如下

  1. 確定字典欄位,新增轉換後的物件型別介面
  2. 將物件列表進行轉換得到其中字典欄位的所有值
  3. 對字典 id 列表進行去重
  4. 根據 id 列表從後臺獲取到所有的字典資料
  5. 將獲得的字典資料轉換為 id => 字典 的 Map
  6. 遍歷最初的列表,對裡面指定的字典欄位進行轉換

可以看到,上面的步驟雖然不麻煩,但卻十分繁瑣,需要定義額外的型別不說,還很容易發生錯誤。

思路

  • 使用 非同步批處理 + 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 中的批處理和快取的資料請關注我們其它相關文章!