You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constgenerateKeyError=newError("Can't generate key from function argument")// 基于函数参数生成唯一值exportdefaultfunctiongenerateKey(argument: any[]): string{try{return`${Array.from(argument).join(',')}`}catch(_){throwgenerateKeyError}}
两年前,我写了一篇关于业务缓存的博客 前端 api 请求缓存方案, 这篇博客反响还不错,其中介绍了如何缓存数据,Promise 以及如何超时删除(也包括如何构建修饰器)。如果对此不够了解,可以阅读博客进行学习。
但之前的代码和方案终归还是简单了些,而且对业务有很大的侵入性。这样不好,于是笔者开始重新学习与思考代理器 Proxy。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。关于 Proxy 的介绍与使用,建议大家还是看阮一峰大神的 ECMAScript 6 入门 代理篇。
项目演进
任何项目都不是一触而就的,下面是关于 Proxy 缓存库的编写思路。希望能对大家有一些帮助。
proxy handler 添加缓存
当然,其实代理器中的 handler 参数也是一个对象,那么既然是对象,当然可以添加数据项,如此,我们便可以基于 Map 缓存编写 memoize 函数用来提升算法递归性能。
我们可以尝试 memoize fibonacci 函数,经过了代理器的函数有非常大的性能提升(肉眼可见):
自定义函数参数
我们仍旧可以利用之前博客介绍的的函数生成唯一值,只不过我们不再需要函数名了:
虽然库本身可以基于函数参数提供唯一值,但是针对形形色色的不同业务来说,这肯定是不够用的,需要提供用户可以自定义参数序列化。
添加 Promise 缓存
在之前的博客中,提到缓存数据的弊端。同一时刻多次调用,会因为请求未返回而进行多次请求。所以我们也需要添加关于 Promise 的缓存。
此时,我们不但可以缓存数据,还可以缓存 Promise 数据请求。
添加过期删除功能
我们可以在数据中添加当前缓存时的时间戳,在生成数据时候添加。
到达这一步,我们可以做到之前博客所描述的所有功能。不过,如果到这里就结束的话,太不过瘾了。我们继续学习其他库的功能来优化我的功能库。
添加手动管理
通常来说,这些缓存库都会有手动管理的功能,所以这里我也提供了手动管理缓存以便业务管理。这里我们使用 Proxy get 方法来拦截属性读取。
当前情况并不复杂,我们可以直接调用,复杂的情况下还是建议使用 Reflect 。
添加 WeakMap
我们在使用 cache 时候,我们同时也可以提供 WeakMap ( WeakMap 没有 clear 和 size 方法),这里我提取了 BaseCache 基类。
之后,我添加各种类型的缓存类都以此为基类。
添加清理函数
在缓存进行删除时候需要对值进行清理,需要用户提供 dispose 函数。该类继承 BaseCache 同时提供 dispose 调用。
当前的缓存如果是 WeakMap,是没有 clear 方法和迭代器的。个人想要添加中间层来完成这一切(还在考虑,目前没有做)。如果 WeakMap 调用 clear 方法时,我是直接提供新的 WeakMap 。
添加计数引用
在学习其他库 memoizee 的过程中,我看到了如下用法:
于是我有样学样,也添加了 RefCache。
同时修改 proxy 主函数:
添加 LRU
LRU 的英文全称是 Least Recently Used,也即最不经常使用。相比于其他的数据结构进行缓存,LRU 无疑更加有效。
这里考虑在添加 maxAge 的同时也添加 max 值 (这里我利用两个 Map 来做 LRU,虽然会增加一定的内存消耗,但是性能更好)。
如果当前的此时保存的数据项等于 max ,我们直接把当前 cacheMap 设为 oldCacheMap,并重新 new cacheMap。
重点在与获取数据时候,如果当前的 cacheMap 中有值且没有过期,直接返回,如果没有,就去 oldCacheMap 查找,如果有,删除老数据并放入新数据(使用 _set 方法),如果都没有,返回 undefined.
整理 memoize 函数
事情到了这一步,我们就可以从之前的代码细节中解放出来了,看看基于这些功能所做出的接口与主函数。
最终的 memoize 函数其实和最开始的函数差不多,只做了 3 件事
完整代码在 memoizee-proxy 中。大家自行操作与把玩。
下一步
测试
测试覆盖率不代表一切,但是在实现库的过程中,JEST 测试库给我提供了大量的帮助,它帮助我重新思考每一个类以及每一个函数应该具有的功能与参数校验。之前的代码我总是在项目的主入口进行校验,对于每个类或者函数的参数没有深入思考。事实上,这个健壮性是不够的。因为你不能决定用户怎么使用你的库。
Proxy 深入
事实上,代理的应用场景是不可限量的。这一点,ruby 已经验证过了(可以去学习《ruby 元编程》)。
开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。
当然,Proxy 虽然来自于 ES6 ,但该 API 仍需要较高的浏览器版本,虽然有 proxy-pollfill ,但毕竟提供功能有限。不过已经 2021,相信深入学习 Proxy 也是时机了。
深入缓存
缓存是有害的!这一点毋庸置疑。但是它实在太快了!所以我们要更加理解业务,哪些数据需要缓存,理解那些数据可以使用缓存。
当前书写的缓存仅仅只是针对与一个方法,之后写的项目是否可以更细粒度的结合返回数据?还是更往上思考,写出一套缓存层?
小步开发
在开发该项目的过程中,我采用小步快跑的方式,不断返工。最开始的代码,也仅仅只到了添加过期删除功能那一步。
但是当我每次完成一个新的功能后,重新开始整理库的逻辑与流程,争取每一次的代码都足够优雅。同时因为我不具备第一次编写就能通盘考虑的能力。不过希望在今后的工作中,不断进步。这样也能减少代码的返工。
其他
函数创建
事实上,我在为当前库添加手动管理时候,考虑过直接复制函数,因为函数本身是一个对象。同时为当前函数添加 set 等方法。但是没有办法把作用域链拷贝过去。
虽然没能成功,但是也学到了一些知识,这里也提供两个创建函数的代码。
我们在创建函数时候基本上会利用 new Function 创建函数,但是浏览器没有提供可以直接创建异步函数的构造器,我们需要手动获取。
对于全局函数,我们也可以直接 fn.toString() 来创建函数,这时候异步函数也可以直接构造的。
参考资料
前端 api 请求缓存方案
ECMAScript 6 入门 代理篇
memoizee
memoizee-proxy
The text was updated successfully, but these errors were encountered: