场景
最近在生产环境遇到了下面这样一个场景:
后台在字典表中存储了一些之前需要前后端共同维护的枚举值,并提供根据 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 中的批处理和缓存的资料请关注其它相关文章!
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
- 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
- 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
- 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
- 群星《2024好听新歌42》AI调整音效【WAV分轨】
- 王思雨-《思念陪着鸿雁飞》WAV
- 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
- 李健《无时无刻》[WAV+CUE][590M]
- 陈奕迅《酝酿》[WAV分轨][502M]
- 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
- 群星《吉他王(黑胶CD)》[WAV+CUE]
- 齐秦《穿乐(穿越)》[WAV+CUE]
- 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
- 邝美云《邝美云精装歌集》[DSF][1.6G]
- 吕方《爱一回伤一回》[WAV+CUE][454M]