手寫一個基于 Proxy 的緩存庫
Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
項目演進
任何項目都不是一觸而就的,下面是關于 Proxy 緩存庫的編寫思路。希望能對大家有一些幫助。
proxy handler 添加緩存
當然,其實代理器中的 handler 參數(shù)也是一個對象,那么既然是對象,當然可以添加數(shù)據(jù)項,如此,我們便可以基于 Map 緩存編寫 memoize 函數(shù)用來提升算法遞歸性能。
type?TargetFun?=?(...args:?any[])?=>?V
function?memoize(fn:?TargetFun )?{
??return?new?Proxy(fn,?{
????//?此處目前只能略過?或者?添加一個中間層集成 Proxy 和?對象。
????//?在對象中添加?cache
????//?@ts-ignore
????cache:?new?Map(),
????apply(target,?thisArg,?argsList)?{
??????//?獲取當前的?cache
??????const?currentCache?=?(this?as?any).cache
??????
??????//?根據(jù)數(shù)據(jù)參數(shù)直接生成?Map?的?key
??????let?cacheKey?=?argsList.toString();
??????
??????//?當前沒有被緩存,執(zhí)行調(diào)用,添加緩存
??????if?(!currentCache.has(cacheKey))?{
????????currentCache.set(cacheKey,?target.apply(thisArg,?argsList));
??????}
??????
??????//?返回被緩存的數(shù)據(jù)
??????return?currentCache.get(cacheKey);
????}
??});
}
我們可以嘗試 memoize fibonacci 函數(shù),經(jīng)過了代理器的函數(shù)有非常大的性能提升(肉眼可見):
const?fibonacci?=?(n:?number):?number?=>?(n?<=?1???1?:?fibonacci(n?-?1)?+?fibonacci(n?-?2));
const?memoizedFibonacci?=?memoize(fibonacci);
for?(let?i?=?0;?i?100;?i++)?fibonacci(30);?//?~5000ms
for?(let?i?=?0;?i?100;?i++)?memoizedFibonacci(30);?//?~50ms
自定義函數(shù)參數(shù)
我們?nèi)耘f可以利用之前博客介紹的的函數(shù)生成唯一值,只不過我們不再需要函數(shù)名了:
const?generateKeyError?=?new?Error("Can't?generate?key?from?function?argument")
//?基于函數(shù)參數(shù)生成唯一值
export?default?function?generateKey(argument:?any[]):?string?{
??try{
????return?`${Array.from(argument).join(',')}`
??}catch(_)?{
????throw?generateKeyError
??}
}
雖然庫本身可以基于函數(shù)參數(shù)提供唯一值,但是針對形形色色的不同業(yè)務來說,這肯定是不夠用的,需要提供用戶可以自定義參數(shù)序列化。
//?如果配置中有?normalizer?函數(shù),直接使用,否則使用默認函數(shù)
const?normalizer?=?options?.normalizer????generateKey
return?new?Proxy(fn,?{
??//?@ts-ignore
??cache,
??apply(target,?thisArg,?argsList:?any[])?{
????const?cache:?Map?=?(this?as?any).cache
????
????//?根據(jù)格式化函數(shù)生成唯一數(shù)值
????const?cacheKey:?string?=?normalizer(argsList);
????
????if?(!cache.has(cacheKey))
??????cache.set(cacheKey,?target.apply(thisArg,?argsList));
????return?cache.get(cacheKey);
??}
});
添加 Promise 緩存
在之前的博客中,提到緩存數(shù)據(jù)的弊端。同一時刻多次調(diào)用,會因為請求未返回而進行多次請求。所以我們也需要添加關于 Promise 的緩存。
if?(!currentCache.has(cacheKey)){
??let?result?=?target.apply(thisArg,?argsList)
??
??//?如果是 promise 則緩存 promise,簡單判斷!
??//?如果當前函數(shù)有?then?則是?Promise
??if?(result?.then)?{
????result?=?Promise.resolve(result).catch(error?=>?{
??????//?發(fā)生錯誤,刪除當前?promise,否則會引發(fā)二次錯誤
??????//?由于異步,所以當前?delete?調(diào)用一定在?set?之后,
??????currentCache.delete(cacheKey)
????
??????//?把錯誤衍生出去
??????return?Promise.reject(error)
????})
??}
??currentCache.set(cacheKey,?result);
}
return?currentCache.get(cacheKey);
此時,我們不但可以緩存數(shù)據(jù),還可以緩存 Promise 數(shù)據(jù)請求。
添加過期刪除功能
我們可以在數(shù)據(jù)中添加當前緩存時的時間戳,在生成數(shù)據(jù)時候添加。
//?緩存項
export?default?class?ExpiredCacheItem?{
??data:?V;
??cacheTime:?number;
??constructor(data:?V)?{
????this.data?=?data
????//?添加系統(tǒng)時間戳
????this.cacheTime?=?(new?Date()).getTime()
??}
}
//?編輯?Map?緩存中間層,判斷是否過期
isOverTime(name:?string)?{
??const?data?=?this.cacheMap.get(name)
??//?沒有數(shù)據(jù)(因為當前保存的數(shù)據(jù)是?ExpiredCacheItem),所以我們統(tǒng)一看成功超時
??if?(!data)?return?true
??//?獲取系統(tǒng)當前時間戳
??const?currentTime?=?(new?Date()).getTime()
??//?獲取當前時間與存儲時間的過去的秒數(shù)
??const?overTime?=?currentTime?-?data.cacheTime
??//?如果過去的秒數(shù)大于當前的超時時間,也返回?null?讓其去服務端取數(shù)據(jù)
??if?(Math.abs(overTime)?>?this.timeout)?{
????//?此代碼可以沒有,不會出現(xiàn)問題,但是如果有此代碼,再次進入該方法就可以減少判斷。
????this.cacheMap.delete(name)
????return?true
??}
??//?不超時
??return?false
}
//?cache?函數(shù)有數(shù)據(jù)
has(name:?string)?{
??//?直接判斷在?cache?中是否超時
??return?!this.isOverTime(name)
}
到達這一步,我們可以做到之前博客所描述的所有功能。不過,如果到這里就結束的話,太不過癮了。我們繼續(xù)學習其他庫的功能來優(yōu)化我的功能庫。
添加手動管理
通常來說,這些緩存庫都會有手動管理的功能,所以這里我也提供了手動管理緩存以便業(yè)務管理。這里我們使用 Proxy get 方法來攔截屬性讀取。
return?new?Proxy(fn,?{
??//?@ts-ignore
??cache,
??get:?(target:?TargetFun,?property:?string)?=>?{
????
????//?如果配置了手動管理
????if?(options?.manual)?{
??????const?manualTarget?=?getManualActionObjFormCache(cache)
??????
??????//?如果當前調(diào)用的函數(shù)在當前對象中,直接調(diào)用,沒有的話訪問原對象
??????//?即使當前函數(shù)有該屬性或者方法也不考慮,誰讓你配置了手動管理呢。
??????if?(property?in?manualTarget)?{
????????return?manualTarget[property]
??????}
????}
???
????//?當前沒有配置手動管理,直接訪問原對象
????return?target[property]
??},
}
export?default?function?getManualActionObjFormCache(
??cache:?MemoizeCache
):?CacheMap?{
??const?manualTarget?=?Object.create(null)
??
??//?通過閉包添加?set?get?delete?clear?等?cache?操作
??manualTarget.set?=?(key:?string?|?object,?val:?V)?=>?cache.set(key,?val)
??manualTarget.get?=?(key:?string?|?object)?=>?cache.get(key)
??manualTarget.delete?=?(key:?string?|?object)?=>?cache.delete(key)
??manualTarget.clear?=?()?=>?cache.clear!()
??
??return?manualTarget
}
當前情況并不復雜,我們可以直接調(diào)用,復雜的情況下還是建議使用 Reflect 。
添加 WeakMap
我們在使用 cache 時候,我們同時也可以提供 WeakMap ( WeakMap 沒有 clear 和 size 方法),這里我提取了 BaseCache 基類。
export?default?class?BaseCache?{
??readonly?weak:?boolean;
??cacheMap:?MemoizeCache
??constructor(weak:?boolean?=?false)?{
????//?是否使用?weakMap
????this.weak?=?weak
????this.cacheMap?=?this.getMapOrWeakMapByOption()
??}
??//?根據(jù)配置獲取?Map?或者?WeakMap
??getMapOrWeakMapByOption():?Map ?|?WeakMap
之后,我添加各種類型的緩存類都以此為基類。
添加清理函數(shù)
在緩存進行刪除時候需要對值進行清理,需要用戶提供 dispose 函數(shù)。該類繼承 BaseCache 同時提供 dispose 調(diào)用。
export?const?defaultDispose:?DisposeFun?=?()?=>?void?0
export?default?class?BaseCacheWithDispose?extends?BaseCache ?{
??readonly?weak:?boolean
??readonly?dispose:?DisposeFun
??constructor(weak:?boolean?=?false,?dispose:?DisposeFun?=?defaultDispose)?{
????super(weak)
????this.weak?=?weak
????this.dispose?=?dispose
??}
??//?清理單個值(調(diào)用?delete?前調(diào)用)
??disposeValue(value:?V?|?undefined):?void?{
????if?(value)?{
??????this.dispose(value)
????}
??}
??//?清理所有值(調(diào)用?clear?方法前調(diào)用,如果當前?Map?具有迭代器)
??disposeAllValue(cacheMap:?MemoizeCache ):?void?{
????for?(let?mapValue?of?(cacheMap?as?any))?{
??????this.disposeValue(mapValue?.[1])
????}
??}
}
當前的緩存如果是 WeakMap,是沒有 clear 方法和迭代器的。個人想要添加中間層來完成這一切(還在考慮,目前沒有做)。如果 WeakMap 調(diào)用 clear 方法時,我是直接提供新的 WeakMap 。
clear()?{
??if?(this.weak)?{
????this.cacheMap?=?this.getMapOrWeakMapByOption()
??}?else?{
????this.disposeAllValue(this.cacheMap)
????this.cacheMap.clear!()
??}
}
添加計數(shù)引用
在學習其他庫?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?extends?BaseCacheWithDispose ?implements?CacheMap ?{
????//?添加?ref?計數(shù)
??cacheRef:?MemoizeCache
??constructor(weak:?boolean?=?false,?dispose:?DisposeFun?=?()?=>?void?0)?{
????super(weak,?dispose)
????//?根據(jù)配置生成?WeakMap?或者?Map
????this.cacheRef?=?this.getMapOrWeakMapByOption()
??}
??
??//?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)
????return?this
??}
??//?也可以手動添加計數(shù)
??addRef(key:?string?|?object)?{
????if?(!this.cacheMap.has(key))?{
??????return
????}
????const?refCount:?number?|?undefined?=?this.cacheRef.get(key)
????this.cacheRef.set(key,?(refCount????0)?+?1)
??}
??getRefCount(key:?string?|?object)?{
????return?this.cacheRef.get(key)????0
??}
??deleteRef(key:?string?|?object):?boolean?{
????if?(!this.cacheMap.has(key))?{
??????return?false
????}
????const?refCount:?number?=?this.getRefCount(key)
????if?(refCount?<=?0)?{
??????return?false
????}
????const?currentRefCount?=?refCount?-?1
????
????//?如果當前?refCount?大于?0,?設置,否則清除
????if?(currentRefCount?>?0)?{
??????this.cacheRef.set(key,?currentRefCount)
????}?else?{
??????this.cacheRef.delete(key)
??????this.cacheMap.delete(key)
????}
????return?true
??}
}
同時修改 proxy 主函數(shù):
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)?{
??//?如果被再次調(diào)用且當前已經(jīng)緩存過了,直接增加???????
??currentCache.addRef?.(cacheKey)
}
添加 LRU
LRU 的英文全稱是 Least Recently Used,也即最不經(jīng)常使用。相比于其他的數(shù)據(jù)結構進行緩存,LRU 無疑更加有效。
這里考慮在添加 maxAge 的同時也添加 max 值 (這里我利用兩個 Map 來做 LRU,雖然會增加一定的內(nèi)存消耗,但是性能更好)。
如果當前的此時保存的數(shù)據(jù)項等于 max ,我們直接把當前 cacheMap 設為 oldCacheMap,并重新 new cacheMap。
set(key:?string?|?object,?value:?V)?{
??const?itemCache?=?new?ExpiredCacheItem(value)
??//?如果之前有值,直接修改
??this.cacheMap.has(key)???this.cacheMap.set(key,?itemCache)?:?this._set(key,?itemCache);
??return?this
}
private?_set(key:?string?|?object,?value:?ExpiredCacheItem)?{
??this.cacheMap.set(key,?value);
??this.size++;
??if?(this.size?>=?this.max)?{
????this.size?=?0;
????this.oldCacheMap?=?this.cacheMap;
????this.cacheMap?=?this.getMapOrWeakMapByOption()
??}
}
重點在與獲取數(shù)據(jù)時候,如果當前的 cacheMap 中有值且沒有過期,直接返回,如果沒有,就去 oldCacheMap 查找,如果有,刪除老數(shù)據(jù)并放入新數(shù)據(jù)(使用 _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!))?{
??????//?移動到新的數(shù)據(jù)中并刪除老數(shù)據(jù)
??????this.moveToRecent(key,?item!);
??????return?item!.data?as?V;
????}
??}
??return?undefined
}
private?moveToRecent(key:?string?|?object,?item:?ExpiredCacheItem)?{
??//?老數(shù)據(jù)刪除
??this.oldCacheMap.delete(key);
??
??//?新數(shù)據(jù)設定,重點?。。?!如果當前設定的數(shù)據(jù)等于?max,清空?oldCacheMap,如此,數(shù)據(jù)不會超過?max
??this._set(key,?item);
}
private?getItemValue(key:?string?|?object,?item:?ExpiredCacheItem):?V?|?undefined?{
??//?如果當前設定了?maxAge?就查詢,否則直接返回
??return?this.maxAge???this.getOrDeleteIfExpired(key,?item)?:?item?.data;
}
??
??
private?getOrDeleteIfExpired(key:?string?|?object,?item:?ExpiredCacheItem):?V?|?undefined?{
??const?deleted?=?this.deleteIfExpired(key,?item);
??return?!deleted???item.data?:?undefined;
}
??
private?deleteIfExpired(key:?string?|?object,?item:?ExpiredCacheItem)?{
??if?(this.isOverTime(item))?{
????return?this.delete(key);
??}
??return?false;
}
整理 memoize 函數(shù)
事情到了這一步,我們就可以從之前的代碼細節(jié)中解放出來了,看看基于這些功能所做出的接口與主函數(shù)。
//?面向接口,無論后面還會不會增加其他類型的緩存類
export?interface?BaseCacheMap?{
??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?{
??/**?序列化參數(shù)?*/
??normalizer?:?(args:?any[])?=>?string;
??/**?是否使用?WeakMap?*/
??weak?:?boolean;
??/**?最大毫秒數(shù),過時刪除?*/
??maxAge?:?number;
??/**?最大項數(shù),超過刪除??*/
??max?:?number;
??/**?手動管理內(nèi)存?*/
??manual?:?boolean;
??/**?是否使用引用計數(shù)??*/
??refCounter?:?boolean;
??/**?緩存刪除數(shù)據(jù)時期的回調(diào)?*/
??dispose?:?DisposeFun;
}
//?返回的函數(shù)(攜帶一系列方法)
export?interface?ResultFun?extends?Function?{
??delete?(key:?string?|?object):?boolean;
??get?(key:?string?|?object):?V?|?undefined;
??has?(key:?string?|?object):?boolean;
??set?(key:?string?|?object,?value:?V):?this;
??clear?():?void;
??deleteRef?():?void
}
最終的 memoize 函數(shù)其實和最開始的函數(shù)差不多,只做了 3 件事
檢查參數(shù)并拋出錯誤 根據(jù)參數(shù)獲取合適的緩存 返回代理
export?default?function?memoize(fn:?TargetFun ,?options?:?MemoizeOptions ):?ResultFun ?{
??//?檢查參數(shù)并拋出錯誤
??checkOptionsThenThrowError(options)
??//?修正序列化函數(shù)
??const?normalizer?=?options?.normalizer????generateKey
??let?cache:?MemoizeCache?=?getCacheByOptions (options)
??//?返回代理
??return?new?Proxy(fn,?{
????//?@ts-ignore
????cache,
????get:?(target:?TargetFun,?property:?string)?=>?{
??????//?添加手動管理
??????if?(options?.manual)?{
????????const?manualTarget?=?getManualActionObjFormCache(cache)
????????if?(property?in?manualTarget)?{
??????????return?manualTarget[property]
????????}
??????}
??????return?target[property]
????},
????apply(target,?thisArg,?argsList:?any[]):?V?{
??????const?currentCache:?MemoizeCache?=?(this?as?any).cache
??????const?cacheKey:?string?|?object?=?getKeyFromArguments(argsList,?normalizer,?options?.weak)
??????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);
??????}?else?if?(options?.refCounter)?{
????????currentCache.addRef?.(cacheKey)
??????}
??????return?currentCache.get(cacheKey)?as?V;
????}
??})?as?any
}
下一步
測試
Proxy 深入
深入緩存
小步開發(fā)
其他
函數(shù)創(chuàng)建
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
function?cloneFunction(fn:?(...args:?any[])?=>?T):?(...args:?any[])?=>?T?{
??return?new?Function('return?'+?fn.toString())();
}
點擊左下角閱讀原文,到?SegmentFault 思否社區(qū)?和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺“回復“?入群?”即可加入我們的技術交流群,收獲更多的技術文章~ -?END -

