返回顶部
首页 > 资讯 > 前端开发 > html >怎么手写一个基于Proxy的缓存库
  • 817
分享到

怎么手写一个基于Proxy的缓存库

2024-04-02 19:04:59 817人浏览 泡泡鱼
摘要

本篇内容主要讲解“怎么手写一个基于Proxy的缓存库”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么手写一个基于Proxy的缓存库”吧!项目演进任何项目都不是

本篇内容主要讲解“怎么手写一个基于Proxy的缓存库”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么手写一个基于Proxy的缓存库”吧!

项目演进

任何项目都不是一触而就的,下面是关于 Proxy 缓存库的编写思路。希望能对大家有一些帮助。

proxy handler 添加缓存

当然,其实代理器中的 handler 参数也是一个对象,那么既然是对象,当然可以添加数据项,如此,我们便可以基于 Map 缓存编写 memoize  函数用来提升算法递归性能。

type TargetFun<V> = (...args: any[]) => V  function memoize<V>(fn: TargetFun<V>) {   return new Proxy(fn, {     // 此处目前只能略过 或者 添加一个中间层集成 Proxy 和 对象。     // 在对象中添加 cache     // @ts-ignore     cache: new Map<string, V>(),     apply(target, thisArg, argsList) {       // 获取当前的 cache       const currentCache = (this as any).cache              // 根据数据参数直接生成 Map 的 key       let cacheKey = argsList.toString();              // 当前没有被缓存,执行调用,添加缓存       if (!currentCache.has(cacheKey)) {         currentCache.set(cacheKey, target.apply(thisArg, argsList));       }              // 返回被缓存的数据       return currentCache.get(cacheKey);     }   }); }

我们可以尝试 memoize fibonacci 函数,经过了代理器的函数有非常大的性能提升(肉眼可见):

const fibonacci = (n: number): number => (n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2)); const memoizedFibonacci = memoize<number>(fibonacci);  for (let i = 0; i < 100; i++) fibonacci(30); // ~5000ms for (let i = 0; i < 100; i++) memoizedFibonacci(30); // ~50ms

自定义函数参数

我们仍旧可以利用之前博客介绍的的函数生成唯一值,只不过我们不再需要函数名了:

const generateKeyError = new Error("Can't generate key from function argument")  // 基于函数参数生成唯一值 export default function generateKey(argument: any[]): string {   try{     return `${Array.from(argument).join(',')}`   }catch(_) {     throw generateKeyError   } }

虽然库本身可以基于函数参数提供唯一值,但是针对形形色色的不同业务来说,这肯定是不够用的,需要提供用户可以自定义参数序列化。

// 如果配置中有 nORMalizer 函数,直接使用,否则使用默认函数 const normalizer = options?.normalizer ?? generateKey  return new Proxy<any>(fn, {   // @ts-ignore   cache,   apply(target, thisArg, argsList: any[]) {     const cache: Map<string, any> = (this as any).cache          // 根据格式化函数生成唯一数值     const cacheKey: string = normalizer(argsList);          if (!cache.has(cacheKey))       cache.set(cacheKey, target.apply(thisArg, argsList));     return cache.get(cacheKey);   } });

添加 Promise 缓存

在之前的博客中,提到缓存数据的弊端。同一时刻多次调用,会因为请求未返回而进行多次请求。所以我们也需要添加关于 Promise 的缓存。

if (!currentCache.has(cacheKey)){   let result = target.apply(thisArg, argsList)      // 如果是 promise 则缓存 promise,简单判断!    // 如果当前函数有 then 则是 Promise   if (result?.then) {     result = Promise.resolve(result).catch(error => {       // 发生错误,删除当前 promise,否则会引发二次错误       // 由于异步,所以当前 delete 调用一定在 set 之后,       currentCache.delete(cacheKey)            // 把错误衍生出去       return Promise.reject(error)     })   }   currentCache.set(cacheKey, result); } return currentCache.get(cacheKey);

此时,我们不但可以缓存数据,还可以缓存 Promise 数据请求。

添加过期删除功能

我们可以在数据中添加当前缓存时的时间戳,在生成数据时候添加。

// 缓存项 export default class ExpiredCacheItem<V> {   data: V;   cacheTime: number;    constructor(data: V) {     this.data = data     // 添加系统时间戳     this.cacheTime = (new Date()).getTime()   } }  // 编辑 Map 缓存中间层,判断是否过期 isOverTime(name: string) {   const data = this.cacheMap.get(name)    // 没有数据(因为当前保存的数据是 ExpiredCacheItem),所以我们统一看成功超时   if (!data) return true    // 获取系统当前时间戳   const currentTime = (new Date()).getTime()    // 获取当前时间与存储时间的过去的秒数   const overTime = currentTime - data.cacheTime    // 如果过去的秒数大于当前的超时时间,也返回 null 让其去服务端取数据   if (Math.abs(overTime) > this.timeout) {

到达这一步,我们可以做到之前博客所描述的所有功能。不过,如果到这里就结束的话,太不过瘾了。我们继续学习其他库的功能来优化我的功能库。

添加手动管理

通常来说,这些缓存库都会有手动管理的功能,所以这里我也提供了手动管理缓存以便业务管理。这里我们使用 Proxy get 方法来拦截属性读取。

 return new Proxy(fn, {   // @ts-ignore   cache,   get: (target: TargetFun<V>, property: string) => {          // 如果配置了手动管理     if (options?.manual) {       const manualTarget = getManualActionObjFormCache<V>(cache)              // 如果当前调用的函数在当前对象中,直接调用,没有的话访问原对象       // 即使当前函数有该属性或者方法也不考虑,谁让你配置了手动管理呢。       if (property in manualTarget) {         return manualTarget[property]       }     }         // 当前没有配置手动管理,直接访问原对象     return target[property]   }, }   export default function getManualActionObjFormCache<V>(   cache: MemoizeCache<V> ): CacheMap<string | object, V> {   const manualTarget = Object.create(null)

当前情况并不复杂,我们可以直接调用,复杂的情况下还是建议使用 Reflect 。

添加 WeakMap

我们在使用 cache 时候,我们同时也可以提供 WeakMap ( WeakMap 没有 clear 和 size 方法),这里我提取了  BaseCache 基类。

export default class BaseCache<V> {   readonly weak: boolean;   cacheMap: MemoizeCache<V>    constructor(weak: boolean = false) {     // 是否使用 weakMap     this.weak = weak     this.cacheMap = this.getMapOrWeakMapByOption()   }    // 根据配置获取 Map 或者 WeakMap   getMapOrWeakMapByOption<T>(): Map<string, T> | WeakMap<object, T>  {     return this.weak ? new WeakMap<object, T>() : new Map<string, T>()   } }

之后,我添加各种类型的缓存类都以此为基类。

添加清理函数

在缓存进行删除时候需要对值进行清理,需要用户提供 dispose 函数。该类继承 BaseCache 同时提供 dispose 调用。

export const defaultDispose: DisposeFun<any> = () => void 0  export default class BaseCacheWithDispose<V, WrapperV> extends BaseCache<WrapperV> {   readonly weak: boolean   readonly dispose: DisposeFun<V>    constructor(weak: boolean = false, dispose: DisposeFun<V> = defaultDispose) {     super(weak)     this.weak = weak     this.dispose = dispose   }    // 清理单个值(调用 delete 前调用)   disposeValue(value: V | undefined): void {     if (value) {       this.dispose(value)     }   }    // 清理所有值(调用 clear 方法前调用,如果当前 Map 具有迭代器)   disposeAllValue<V>(cacheMap: MemoizeCache<V>): void {     for (let mapValue of (cacheMap as any)) {       this.disposeValue(mapValue?.[1])     }   } }

当前的缓存如果是 WeakMap,是没有 clear 方法和迭代器的。个人想要添加中间层来完成这一切(还在考虑,目前没有做)。如果 WeakMap 调用  clear 方法时,我是直接提供新的 WeakMap 。

clear() {   if (this.weak) {     this.cacheMap = this.getMapOrWeakMapByOption()   } else {     this.disposeAllValue(this.cacheMap)     this.cacheMap.clear!()   } }

添加计数引用

在学习其他库 memoizee 的过程中,我看到了如下用法:

memoized = memoize(fn, { refCounter: true });  memoized("foo", 3); // refs: 1 memoized("foo", 3); // Cache hit, refs: 2 memoized("foo", 3); // Cache hit, refs: 3 memoized.deleteRef("foo", 3); // refs: 2 memoized.deleteRef("foo", 3); // refs: 1 memoized.deleteRef("foo", 3); // refs: 0,清除 foo 的缓存 memoized("foo", 3); // Re-executed, refs: 1

于是我有样学样,也添加了 RefCache。

export default class RefCache<V> extends BaseCacheWithDispose<V, V> implements CacheMap<string | object, V> {     // 添加 ref 计数   cacheRef: MemoizeCache<number>    constructor(weak: boolean = false, dispose: DisposeFun<V> = () => void 0) {     super(weak, dispose)     // 根据配置生成 WeakMap 或者 Map     this.cacheRef = this.getMapOrWeakMapByOption<number>()   }       // get has clear 等相同。不列出      delete(key: string | object): boolean {     this.disposeValue(this.get(key))     this.cacheRef.delete(key)     this.cacheMap.delete(key)     return true;   }     set(key: string | object, value: V): this {     this.cacheMap.set(key, value)     // set 的同时添加 ref     this.addRef(key)

同时修改 proxy 主函数:

if (!currentCache.has(cacheKey)) {   let result = target.apply(thisArg, argsList)    if (result?.then) {     result = Promise.resolve(result).catch(error => {       currentCache.delete(cacheKey)       return Promise.reject(error)     })   }   currentCache.set(cacheKey, result);    // 当前配置了 refCounter } else if (options?.refCounter) {   // 如果被再次调用且当前已经缓存过了,直接增加          currentCache.addRef?.(cacheKey) }

添加 LRU

LRU 的英文全称是 Least Recently Used,也即最不经常使用。相比于其他的数据结构进行缓存,LRU 无疑更加有效。

这里考虑在添加 maxAge 的同时也添加 max 值 (这里我利用两个 Map 来做 LRU,虽然会增加一定的内存消耗,但是性能更好)。

如果当前的此时保存的数据项等于 max ,我们直接把当前 cacheMap 设为 oldCacheMap,并重新 new cacheMap。

set(key: string | object, value: V) {   const itemCache = new ExpiredCacheItem<V>(value)   // 如果之前有值,直接修改   this.cacheMap.has(key) ? this.cacheMap.set(key, itemCache) : this._set(key, itemCache);   return this }  private _set(key: string | object, value: ExpiredCacheItem<V>) {   this.cacheMap.set(key, value);   this.size++;    if (this.size >= this.max) {     this.size = 0;     this.oldCacheMap = this.cacheMap;     this.cacheMap = this.getMapOrWeakMapByOption()   } }

重点在与获取数据时候,如果当前的 cacheMap 中有值且没有过期,直接返回,如果没有,就去 oldCacheMap  查找,如果有,删除老数据并放入新数据(使用 _set 方法),如果都没有,返回 undefined.

get(key: string | object): V | undefined {   // 如果 cacheMap 有,返回 value   if (this.cacheMap.has(key)) {     const item = this.cacheMap.get(key);     return this.getItemValue(key, item!);   }    // 如果 oldCacheMap 里面有   if (this.oldCacheMap.has(key)) {     const item = this.oldCacheMap.get(key);     // 没有过期     if (!this.deleteIfExpired(key, item!)) {       // 移动到新的数据中并删除老数据       this.moveToRecent(key, item!);       return item!.data as V;     }   }   return undefined }   private moveToRecent(key: string | object, item: ExpiredCacheItem<V>) {   // 老数据删除   this.oldCacheMap.delete(key);

整理 memoize 函数

事情到了这一步,我们就可以从之前的代码细节中解放出来了,看看基于这些功能所做出的接口与主函数。

// 面向接口,无论后面还会不会增加其他类型的缓存类 export interface BaseCacheMap<K, V> {   delete(key: K): boolean;    get(key: K): V | undefined;    has(key: K): boolean;    set(key: K, value: V): this;    clear?(): void;    addRef?(key: K): void;    deleteRef?(key: K): boolean; }  // 缓存配置 export interface MemoizeOptions<V> {      normalizer?: (args: any[]) => string;      weak?: boolean;      maxAge?: number;   

最终的 memoize 函数其实和最开始的函数差不多,只做了 3 件事

  • 检查参数并抛出错误

  • 根据参数获取合适的缓存

  • 返回代理

export default function memoize<V>(fn: TargetFun<V>, options?: MemoizeOptions<V>): ResultFun<V> {   // 检查参数并抛出错误   checkOptionsThenThrowError<V>(options)    // 修正序列化函数   const normalizer = options?.normalizer ?? generateKey    let cache: MemoizeCache<V> = getCacheByOptions<V>(options)    // 返回代理   return new Proxy(fn, {     // @ts-ignore     cache,     get: (target: TargetFun<V>, property: string) => {       // 添加手动管理       if (options?.manual) {         const manualTarget = getManualActionObjFormCache<V>(cache)         if (property in manualTarget) {           return manualTarget[property]         }       }       return target[property]     },     apply(target, thisArg, argsList: any[]): V {

完整代码在 memoizee-proxy 中。大家自行操作与把玩。

下一步

测试

测试覆盖率不代表一切,但是在实现库的过程中,JEST  测试库给我提供了大量的帮助,它帮助我重新思考每一个类以及每一个函数应该具有的功能与参数校验。之前的代码我总是在项目的主入口进行校验,对于每个类或者函数的参数没有深入思考。事实上,这个健壮性是不够的。因为你不能决定用户怎么使用你的库。

Proxy 深入

事实上,代理的应用场景是不可限量的。这一点,ruby 已经验证过了(可以去学习《ruby 元编程》)。

开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。

当然,Proxy 虽然来自于 es6 ,但该 api 仍需要较高的浏览器版本,虽然有 proxy-pollfill ,但毕竟提供功能有限。不过已经  2021,相信深入学习 Proxy 也是时机了。

深入缓存

缓存是有害的!这一点毋庸置疑。但是它实在太快了!所以我们要更加理解业务,哪些数据需要缓存,理解那些数据可以使用缓存。

当前书写的缓存仅仅只是针对与一个方法,之后写的项目是否可以更细粒度的结合返回数据?还是更往上思考,写出一套缓存层?

小步开发

在开发该项目的过程中,我采用小步快跑的方式,不断返工。最开始的代码,也仅仅只到了添加过期删除功能那一步。

但是当我每次完成一个新的功能后,重新开始整理库的逻辑与流程,争取每一次的代码都足够优雅。同时因为我不具备第一次编写就能通盘考虑的能力。不过希望在今后的工作中,不断进步。这样也能减少代码的返工。

其他

函数创建

事实上,我在为当前库添加手动管理时候,考虑过直接复制函数,因为函数本身是一个对象。同时为当前函数添加 set  等方法。但是没有办法把作用域链拷贝过去。

虽然没能成功,但是也学到了一些知识,这里也提供两个创建函数的代码。

我们在创建函数时候基本上会利用 new Function 创建函数,但是浏览器没有提供可以直接创建异步函数的构造器,我们需要手动获取。

AsyncFunction = (async x => x).constructor  foo = new AsyncFunction('x, y, p', 'return x + y + await p')  foo(1,2, Promise.resolve(3)).then(console.log) // 6

对于全局函数,我们也可以直接 fn.toString() 来创建函数,这时候异步函数也可以直接构造的。

function cloneFunction<T>(fn: (...args: any[]) => T): (...args: any[]) => T {   return new Function('return '+ fn.toString())(); }

到此,相信大家对“怎么手写一个基于Proxy的缓存库”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: 怎么手写一个基于Proxy的缓存库

本文链接: https://lsjlt.com/news/83576.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 怎么手写一个基于Proxy的缓存库
    本篇内容主要讲解“怎么手写一个基于Proxy的缓存库”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么手写一个基于Proxy的缓存库”吧!项目演进任何项目都不是...
    99+
    2024-04-02
  • 基于SpringCloud手写一个简易版Sentinel
    目录Sentinel 是什么?定义注解定义切面处理器测试降级Sentinel 是什么? 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流...
    99+
    2024-04-02
  • Python怎样手动编写一个自己的LRU缓存装饰器
    Python怎样手动编写一个自己的LRU缓存装饰器,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。LRU缓存算法,指的是近期最少使用算法,大体逻辑就是淘汰最长时间没有用的那个缓存...
    99+
    2023-06-22
  • 基于Python怎么编写一个点名器
    这篇文章主要介绍“基于Python怎么编写一个点名器”,在日常操作中,相信很多人在基于Python怎么编写一个点名器问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”基于Python怎么编写一个点名器”的疑惑有所...
    99+
    2023-07-02
  • JavaScript手写一个前端存储工具库
    目录使用 storage-tools 缓存数据storage-tools 项目演进StorageHelper 支持 localStorage 存储StorageHelper 添加超时...
    99+
    2023-02-14
    JavaScript手写前端存储工具库 JavaScript前端存储工具 JavaScript前端存储
  • 基于C++怎么编写一个Json解析器
    这篇文章主要介绍了基于C++怎么编写一个Json解析器的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇基于C++怎么编写一个Json解析器文章都会有所收获,下面我们一起来看看吧。代码JsonSerialize.h...
    99+
    2023-07-05
  • 基于Java手写一个好用的FTP操作工具类
    目录前言windows服务器搭建FTP服务工具类方法代码展示使用示例前言 网上百度了很多FTP的java 工具类,发现文章代码都比较久远,且代码臃肿,即使搜到了代码写的还可以的,封装...
    99+
    2024-04-02
  • 怎么保证缓存与数据库的双写一致性
    本篇内容介绍了“怎么保证缓存与数据库的双写一致性”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Cache ...
    99+
    2024-04-02
  • 基于C++怎么编写一个简单的服务器
    这篇文章主要讲解了“基于C++怎么编写一个简单的服务器”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“基于C++怎么编写一个简单的服务器”吧!先写个简易的controller基类继承反射基类,...
    99+
    2023-07-05
  • 基于Python怎么编写一个语音合成系统
    这篇文章主要介绍了基于Python怎么编写一个语音合成系统的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇基于Python怎么编写一个语音合成系统文章都会有所收获,下面我们一起来看看吧。背景一直对语音合成系统比较...
    99+
    2023-06-29
  • 基于Python怎么编写一个刷题练习系统
    这篇“基于Python怎么编写一个刷题练习系统”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“基于Python怎么编写一个刷题...
    99+
    2023-07-05
  • 基于Python怎么编写一个二维码生成器
    这篇“基于Python怎么编写一个二维码生成器”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“基于Python怎么编写一个二维...
    99+
    2023-07-02
  • 基于WPF怎么编写一个串口转UDP工具
    这篇“基于WPF怎么编写一个串口转UDP工具”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“基于WPF怎么编写一个串口转UDP...
    99+
    2023-07-05
  • 基于Java怎么实现Redis多级缓存
    这篇文章主要介绍“基于Java怎么实现Redis多级缓存”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“基于Java怎么实现Redis多级缓存”文章能帮助大家解决问题。一、多级缓存1. 传统缓存方案请...
    99+
    2023-06-29
  • mybatis的一级缓存怎么手动刷新
    MyBatis的一级缓存是自动的,不需要手动刷新。一级缓存是在同一个SqlSession中有效的,当执行了一次查询后,查询结果会被缓...
    99+
    2023-10-26
    mybatis
  • 基于Python怎么编写一个监控CPU的应用系统
    这篇“基于Python怎么编写一个监控CPU的应用系统”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“基于Python怎么编写...
    99+
    2023-07-02
  • redis怎么缓存整个数据库
    要将整个数据库缓存到Redis中,可以使用Redis的持久化机制。Redis提供了两种持久化方法:RDB(Redis Databas...
    99+
    2023-08-23
    redis 数据库
  • 怎么手写一个Spring Boot Starter
    这篇文章将为大家详细讲解有关怎么手写一个Spring Boot Starter,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。何为 Starter ?想必大家都使用过 SpringBoot,在 Spring...
    99+
    2023-06-06
  • 利用Python编写一个简单的缓存系统
    目录项目展示不用落地的缓存系统系统应该如何实现铺垫python字典基本操作定义一个不用落地的缓存系统数据如何落地将对象保存到磁盘将对象从磁盘中导入到内存中将数据落地和缓存系统结合起来...
    99+
    2023-05-16
    Python实现缓存系统 Python缓存系统 Python缓存
  • 基于Python怎么编写一个微博抽奖小程序
    本篇内容主要讲解“基于Python怎么编写一个微博抽奖小程序”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“基于Python怎么编写一个微博抽奖小程序”吧!开发工具Python版本:3.7.8相关...
    99+
    2023-06-30
软考高级职称资格查询
推荐阅读
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作