详解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 学习笔记 Black.Caffeine 09.11.28
Nov 30 Javascript
jQuery 表格工具集
Apr 25 Javascript
Jquery实现瀑布流布局(备有详细注释)
Jul 31 Javascript
JS运动相关知识点小结(附弹性运动示例)
Jan 08 Javascript
基于javascript实现泡泡大冒险网页版小游戏
Mar 23 Javascript
vue实现手机号码抽奖上下滚动动画示例
Oct 18 Javascript
select2 ajax 设置默认值,初始值的方法
Aug 09 Javascript
jquery+ajax实现上传图片并显示上传进度功能【附php后台接收】
Jun 06 jQuery
利用d3.js实现蜂巢图表带动画效果
Sep 03 Javascript
详解Vue串联过滤器的使用场景
Apr 30 Javascript
微信小程序实现自定义底部导航
Nov 18 Javascript
ant design charts 获取后端接口数据展示
May 25 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 smarty函数扩展
2010/03/15 PHP
Laravel 5框架学习之用户认证
2015/04/09 PHP
Laravel 实现密码重置功能
2018/02/23 PHP
window.open的功能全解析
2006/10/10 Javascript
Javascript的一种模块模式
2008/03/22 Javascript
file模式访问网页时iframe高度自适应解决方案
2013/01/16 Javascript
JS打开层/关闭层/移动层动画效果的实例代码
2013/05/11 Javascript
js实现收缩菜单效果实例代码
2013/10/30 Javascript
js charAt的使用示例
2014/02/18 Javascript
JavaScript中setMonth()方法的使用详解
2015/06/11 Javascript
跟我学习javascript的prototype,getPrototypeOf和__proto__
2015/11/17 Javascript
基于jquery实现省市联动效果
2015/11/23 Javascript
AngularJS基础 ng-mouseover 指令简单示例
2016/08/02 Javascript
如何在vue里添加好看的lottie动画
2018/08/02 Javascript
vue print.js打印支持Echarts图表操作
2020/11/13 Javascript
[02:08]DOTA2英雄基础教程 马格纳斯
2014/01/17 DOTA
使用Python的Twisted框架编写非阻塞程序的代码示例
2016/05/25 Python
Python处理文本文件中控制字符的方法
2017/02/07 Python
Django框架实现的普通登录案例【使用POST方法】
2019/05/15 Python
用Python配平化学方程式的方法
2019/07/20 Python
python+tkinter实现学生管理系统
2019/08/20 Python
Python实现图片裁剪的两种方式(Pillow和OpenCV)
2019/10/30 Python
python反编译教程之2048小游戏实例
2021/03/03 Python
CSS3教程(2):网页边框半径和网页圆角
2009/04/02 HTML / CSS
CSS3实现圆角、阴影、透明效果并兼容各大浏览器
2014/08/08 HTML / CSS
HTML5地理定位_动力节点Java学院整理
2017/07/12 HTML / CSS
纽约服装和生活方式品牌:Saturdays NYC
2017/08/13 全球购物
Rakuten Kobo台湾:电子书、eReaders和Reading应用程式
2017/11/24 全球购物
Sneaker Studio乌克兰:购买运动鞋
2018/03/26 全球购物
英国历史最悠久的DJ设备供应商:DJ Finance、DJ Warehouse、The DJ Shop
2019/09/04 全球购物
出纳员岗位责任制
2014/02/11 职场文书
《散步》教学反思
2014/03/02 职场文书
《九寨沟》教学反思
2014/04/08 职场文书
党员示范岗材料
2014/12/19 职场文书
员工自我工作评价
2015/03/06 职场文书
政府会议通知范文
2015/04/15 职场文书