详解JavaScript 中的批处理和缓存


Posted in Javascript onNovember 19, 2020

场景

最近在生产环境遇到了下面这样一个场景:
后台在字典表中存储了一些之前需要前后端共同维护的枚举值,并提供根据 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 中的批处理和缓存的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
JavaScript使用技巧精萃[代码非常实用]
Nov 21 Javascript
javascript 模拟点击广告
Jan 02 Javascript
超级24小时弹窗代码 24小时退出弹窗代码 100%弹窗代码(IE only)
Jun 11 Javascript
js获得当前时区夏令时发生和终止的时间代码
Feb 23 Javascript
JQuery 在线引用及测试引用是否成功
Jun 24 Javascript
jQuery插件Validate实现自定义校验结果样式
Jan 18 Javascript
关于Node.js的events.EventEmitter用法介绍
Apr 01 Javascript
Vue分页组件实例代码
Apr 17 Javascript
javascript 开发之百度地图使用到的js函数整理
May 19 Javascript
详解基于Node.js的HTTP/2 Server实践
May 31 Javascript
vue中的双向数据绑定原理与常见操作技巧详解
Mar 16 Javascript
Ant Design Pro 之 ProTable使用操作
Oct 31 Javascript
Javascript中window.name属性详解
Nov 19 #Javascript
JavaScript实现图片合成下载的示例
Nov 19 #Javascript
vue 获取到数据但却渲染不到页面上的解决方法
Nov 19 #Vue.js
vue 插槽简介及使用示例
Nov 19 #Vue.js
微信小程序实现点击导航条切换页面
Nov 19 #Javascript
详解Vue的mixin策略
Nov 19 #Vue.js
微信小程序自定义底部弹出框功能
Nov 18 #Javascript
You might like
php 目录与文件处理-郑阿奇(续)
2011/07/04 PHP
ThinkPHP与PHPExcel冲突解决方法
2011/08/08 PHP
PHP函数spl_autoload_register()用法和__autoload()介绍
2012/02/04 PHP
ThinkPHP3.0略缩图不能保存到子目录的解决方法
2012/09/30 PHP
CodeIgniter基本配置详细介绍
2013/11/12 PHP
使用PHP实现生成HTML静态页面
2015/11/18 PHP
PHP实现基于回溯法求解迷宫问题的方法详解
2017/08/17 PHP
PHP实现广度优先搜索算法(BFS,Broad First Search)详解
2017/09/16 PHP
JQuery 学习笔记 选择器之一
2009/07/23 Javascript
jQuery 方法大全方便学习参考
2010/02/25 Javascript
javascript 基础篇4 window对象,DOM
2012/03/14 Javascript
JavaScript中实现依赖注入的思路分享
2015/01/15 Javascript
javascript实现html页面之间参数传递的四种方法实例分析
2015/12/15 Javascript
微信小程序中的swiper组件详解
2017/04/14 Javascript
获取本机IP地址的实例(JavaScript / Node.js)
2017/11/24 Javascript
js时间戳与日期格式之间转换详解
2017/12/11 Javascript
elementUI多选框反选的实现代码
2019/04/03 Javascript
Python使用chardet判断字符编码
2015/05/09 Python
听歌识曲--用python实现一个音乐检索器的功能
2016/11/15 Python
在Pycharm中自动添加时间日期作者等信息的方法
2019/01/16 Python
python实现对象列表根据某个属性排序的方法详解
2019/06/11 Python
解决python中使用PYQT时中文乱码问题
2019/06/17 Python
python中的subprocess.Popen()使用详解
2019/12/25 Python
Python使用PyQt5/PySide2编写一个极简的音乐播放器功能
2020/02/07 Python
Python字典添加,删除,查询等相关操作方法详解
2020/02/07 Python
CSS3中的注音对齐属性ruby-align用法指南
2016/07/01 HTML / CSS
德国体育用品网上商店:SC24.com
2016/08/01 全球购物
Quiksilver荷兰官方网站:冲浪和滑雪板
2019/11/16 全球购物
.net笔试题
2014/03/03 面试题
《少年王冕》教学反思
2014/04/11 职场文书
幼儿园教师的考核评语
2014/04/18 职场文书
副校长个人对照检查材料思想汇报
2014/10/04 职场文书
2016教师给学生的毕业寄语
2015/12/04 职场文书
领导激励员工的演讲稿,各种会上用得到,建议收藏
2019/08/13 职场文书
Vue中foreach数组与js中遍历数组的写法说明
2021/06/05 Vue.js
使用 DataAnt 监控 Apache APISIX的原理解析
2022/07/07 Servers