<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于 TypeScript 實現(xiàn)一個基于 Proxy 的緩存庫

          共 15090字,需瀏覽 31分鐘

           ·

          2021-12-01 22:14

          關(guān)注 社全棧前端精選,回復“1”加群

          加入我們一起學習,天天進步

          作者:wsafight

          https://github.com/wsafight/personBlog/issues/34



          兩年前,我寫了一篇關(guān)于業(yè)務緩存的博客 前端 api 請求緩存方案[1], 這篇博客反響還不錯,其中介紹了如何緩存數(shù)據(jù),Promise 以及如何超時刪除(也包括如何構(gòu)建修飾器)。如果對此不夠了解,可以閱讀博客進行學習。

          但之前的代碼和方案終歸還是簡單了些,而且對業(yè)務有很大的侵入性。這樣不好,于是筆者開始重新學習與思考代理器 Proxy。

          Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。關(guān)于 Proxy 的介紹與使用,建議大家還是看阮一峰大神的 ECMAScript 6 入門 代理篇[2]

          項目演進

          任何項目都不是一觸而就的,下面是關(guān)于 Proxy 緩存庫的編寫思路。希望能對大家有一些幫助。

          proxy handler 添加緩存

          當然,其實代理器中的 handler 參數(shù)也是一個對象,那么既然是對象,當然可以添加數(shù)據(jù)項,如此,我們便可以基于 Map 緩存編寫 memoize 函數(shù)用來提升算法遞歸性能。

          type TargetFun<V> = (...args: any[]) => V

          function memoize<V>(fn: TargetFun<V>) {
          returnnew Proxy(fn, {
          // 此處目前只能略過 或者 添加一個中間層集成 Proxy 和 對象。
          // 在對象中添加 cache
          // @ts-ignore
          cache: new Map<string, V>(),
          apply(target, thisArg, argsList) {
          // 獲取當前的 cache
          const currentCache = (thisasany).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<number>(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 = newError("Can't generate key from function argument")

          // 基于函數(shù)參數(shù)生成唯一值
          exportdefaultfunction 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

          returnnew Proxy<any>(fn, {
          // @ts-ignore
          cache,
          apply(target, thisArg, argsList: any[]) {
          const cache: Map<string, any> = (thisasany).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)用,會因為請求未返回而進行多次請求。所以我們也需要添加關(guān)于 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)

          // 把錯誤衍生出去
          returnPromise.reject(error)
          })
          }
          currentCache.set(cacheKey, result);
          }
          return currentCache.get(cacheKey);

          此時,我們不但可以緩存數(shù)據(jù),還可以緩存 Promise 數(shù)據(jù)請求。

          添加過期刪除功能

          我們可以在數(shù)據(jù)中添加當前緩存時的時間戳,在生成數(shù)據(jù)時候添加。

          // 緩存項
          exportdefaultclass ExpiredCacheItem<V> {
          data: V;
          cacheTime: number;

          constructor(data: V) {
          this.data = data
          // 添加系統(tǒng)時間戳
          this.cacheTime = (newDate()).getTime()
          }
          }

          // 編輯 Map 緩存中間層,判斷是否過期
          isOverTime(name: string) {
          const data = this.cacheMap.get(name)

          // 沒有數(shù)據(jù)(因為當前保存的數(shù)據(jù)是 ExpiredCacheItem),所以我們統(tǒng)一看成功超時
          if (!data) returntrue

          // 獲取系統(tǒng)當前時間戳
          const currentTime = (newDate()).getTime()

          // 獲取當前時間與存儲時間的過去的秒數(shù)
          const overTime = currentTime - data.cacheTime

          // 如果過去的秒數(shù)大于當前的超時時間,也返回 null 讓其去服務端取數(shù)據(jù)
          if (Math.abs(overTime) > this.timeout) {
          // 此代碼可以沒有,不會出現(xiàn)問題,但是如果有此代碼,再次進入該方法就可以減少判斷。
          this.cacheMap.delete(name)
          returntrue
          }

          // 不超時
          returnfalse
          }

          // cache 函數(shù)有數(shù)據(jù)
          has(name: string) {
          // 直接判斷在 cache 中是否超時
          return !this.isOverTime(name)
          }

          到達這一步,我們可以做到之前博客所描述的所有功能。不過,如果到這里就結(jié)束的話,太不過癮了。我們繼續(xù)學習其他庫的功能來優(yōu)化我的功能庫。

          添加手動管理

          通常來說,這些緩存庫都會有手動管理的功能,所以這里我也提供了手動管理緩存以便業(yè)務管理。這里我們使用 Proxy get 方法來攔截屬性讀取。

          returnnew Proxy(fn, {
          // @ts-ignore
          cache,
          get: (target: TargetFun<V>, property: string) => {

          // 如果配置了手動管理
          if (options?.manual) {
          const manualTarget = getManualActionObjFormCache<V>(cache)

          // 如果當前調(diào)用的函數(shù)在當前對象中,直接調(diào)用,沒有的話訪問原對象
          // 即使當前函數(shù)有該屬性或者方法也不考慮,誰讓你配置了手動管理呢。
          if (property in manualTarget) {
          return manualTarget[property]
          }
          }

          // 當前沒有配置手動管理,直接訪問原對象
          return target[property]
          },
          }

          exportdefaultfunction getManualActionObjFormCache<V>(
          cache: MemoizeCache<V>
          ): CacheMap<string | object, V>
          {
          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[3] 。

          添加 WeakMap

          我們在使用 cache 時候,我們同時也可以提供 WeakMap ( WeakMap 沒有 clear 和 size 方法),這里我提取了 BaseCache 基類。

          exportdefaultclass BaseCache<V> {
          readonly weak: boolean;
          cacheMap: MemoizeCache<V>

          constructor(weak: boolean = false) {
          // 是否使用 weakMap
          this.weak = weak
          this.cacheMap = this.getMapOrWeakMapByOption()
          }

          // 根據(jù)配置獲取 Map 或者 WeakMap
          getMapOrWeakMapByOption<T>(): Map<string, T> | WeakMap<object, T> {
          returnthis.weak ? new WeakMap<object, T>() : new Map<string, T>()
          }
          }

          之后,我添加各種類型的緩存類都以此為基類。

          添加清理函數(shù)

          在緩存進行刪除時候需要對值進行清理,需要用戶提供 dispose 函數(shù)。該類繼承 BaseCache 同時提供 dispose 調(diào)用。

          exportconst defaultDispose: DisposeFun<any> = () =>void0

          exportdefaultclass 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
          }

          // 清理單個值(調(diào)用 delete 前調(diào)用)
          disposeValue(value: V | undefined): void {
          if (value) {
          this.dispose(value)
          }
          }

          // 清理所有值(調(diào)用 clear 方法前調(diào)用,如果當前 Map 具有迭代器)
          disposeAllValue<V>(cacheMap: MemoizeCache<V>): void {
          for (let mapValue of (cacheMap asany)) {
          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[4] 的過程中,我看到了如下用法:

          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。

          exportdefaultclass RefCache<V> extends BaseCacheWithDispose<V, V> implements CacheMap<string | object, V> {
          // 添加 ref 計數(shù)
          cacheRef: MemoizeCache<number>

          constructor(weak: boolean = false, dispose: DisposeFun<V> = () => void 0) {
          super(weak, dispose)
          // 根據(jù)配置生成 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)
          returntrue;
          }

          set(key: string | object, value: V): this {
          this.cacheMap.set(key, value)
          // set 的同時添加 ref
          this.addRef(key)
          returnthis
          }

          // 也可以手動添加計數(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) {
          returnthis.cacheRef.get(key) ?? 0
          }

          deleteRef(key: string | object): boolean {
          if (!this.cacheMap.has(key)) {
          returnfalse
          }

          const refCount: number = this.getRefCount(key)

          if (refCount <= 0) {
          returnfalse
          }

          const currentRefCount = refCount - 1

          // 如果當前 refCount 大于 0, 設置,否則清除
          if (currentRefCount > 0) {
          this.cacheRef.set(key, currentRefCount)
          } else {
          this.cacheRef.delete(key)
          this.cacheMap.delete(key)
          }
          returntrue
          }
          }

          同時修改 proxy 主函數(shù):

          if (!currentCache.has(cacheKey)) {
          let result = target.apply(thisArg, argsList)

          if (result?.then) {
          result = Promise.resolve(result).catch(error => {
          currentCache.delete(cacheKey)
          returnPromise.reject(error)
          })
          }
          currentCache.set(cacheKey, result);

          // 當前配置了 refCounter
          } elseif (options?.refCounter) {
          // 如果被再次調(diào)用且當前已經(jīng)緩存過了,直接增加
          currentCache.addRef?.(cacheKey)
          }

          添加 LRU

          LRU 的英文全稱是 Least Recently Used,也即最不經(jīng)常使用。相比于其他的數(shù)據(jù)結(jié)構(gòu)進行緩存,LRU 無疑更加有效。

          這里考慮在添加 maxAge 的同時也添加 max 值 (這里我利用兩個 Map 來做 LRU,雖然會增加一定的內(nèi)存消耗,但是性能更好)。

          如果當前的此時保存的數(shù)據(jù)項等于 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);
          returnthis
          }

          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()
          }
          }

          重點在與獲取數(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);
          returnthis.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;
          }
          }
          returnundefined
          }

          private moveToRecent(key: string | object, item: ExpiredCacheItem<V>) {
          // 老數(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>): V | undefined {
          // 如果當前設定了 maxAge 就查詢,否則直接返回
          returnthis.maxAge ? this.getOrDeleteIfExpired(key, item) : item?.data;
          }

          private getOrDeleteIfExpired(key: string | object, item: ExpiredCacheItem<V>): V | undefined {
          const deleted = this.deleteIfExpired(key, item);
          return !deleted ? item.data : undefined;
          }

          private deleteIfExpired(key: string | object, item: ExpiredCacheItem<V>) {
          if (this.isOverTime(item)) {
          returnthis.delete(key);
          }
          returnfalse;
          }

          整理 memoize 函數(shù)

          事情到了這一步,我們就可以從之前的代碼細節(jié)中解放出來了,看看基于這些功能所做出的接口與主函數(shù)。

          // 面向接口,無論后面還會不會增加其他類型的緩存類
          exportinterface 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;
          }

          // 緩存配置
          exportinterface MemoizeOptions<V> {
          /** 序列化參數(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<V>;
          }

          // 返回的函數(shù)(攜帶一系列方法)
          exportinterface ResultFun<V> 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ù)獲取合適的緩存
          • 返回代理
          exportdefaultfunction memoize<V>(fn: TargetFun<V>, options?: MemoizeOptions<V>): ResultFun<V> {
          // 檢查參數(shù)并拋出錯誤
          checkOptionsThenThrowError<V>(options)

          // 修正序列化函數(shù)
          const normalizer = options?.normalizer ?? generateKey

          let cache: MemoizeCache<V> = getCacheByOptions<V>(options)

          // 返回代理
          returnnew 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 {

          const currentCache: MemoizeCache<V> = (thisasany).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)
          returnPromise.reject(error)
          })
          }
          currentCache.set(cacheKey, result);
          } elseif (options?.refCounter) {
          currentCache.addRef?.(cacheKey)
          }
          return currentCache.get(cacheKey) as V;
          }
          }) asany
          }

          完整代碼在 memoizee\-proxy[5] 中。大家自行操作與把玩。

          下一步

          測試

          測試覆蓋率不代表一切,但是在實現(xiàn)庫的過程中,JEST[6] 測試庫給我提供了大量的幫助,它幫助我重新思考每一個類以及每一個函數(shù)應該具有的功能與參數(shù)校驗。之前的代碼我總是在項目的主入口進行校驗,對于每個類或者函數(shù)的參數(shù)沒有深入思考。事實上,這個健壯性是不夠的。因為你不能決定用戶怎么使用你的庫。

          Proxy 深入

          事實上,代理的應用場景是不可限量的。這一點,ruby 已經(jīng)驗證過了(可以去學習《ruby 元編程》)。

          開發(fā)者使用它可以創(chuàng)建出各種編碼模式,比如(但遠遠不限于)跟蹤屬性訪問、隱藏屬性、阻止修改或刪除屬性、函數(shù)參數(shù)驗證、構(gòu)造函數(shù)參數(shù)驗證、數(shù)據(jù)綁定,以及可觀察對象。

          當然,Proxy 雖然來自于 ES6 ,但該 API 仍需要較高的瀏覽器版本,雖然有 proxy-pollfill[7] ,但畢竟提供功能有限。不過已經(jīng) 2021,相信深入學習 Proxy 也是時機了。

          深入緩存

          緩存是有害的!這一點毋庸置疑。但是它實在太快了!所以我們要更加理解業(yè)務,哪些數(shù)據(jù)需要緩存,理解那些數(shù)據(jù)可以使用緩存。

          當前書寫的緩存僅僅只是針對與一個方法,之后寫的項目是否可以更細粒度的結(jié)合返回數(shù)據(jù)?還是更往上思考,寫出一套緩存層?

          小步開發(fā)

          在開發(fā)該項目的過程中,我采用小步快跑的方式,不斷返工。最開始的代碼,也僅僅只到了添加過期刪除功能那一步。

          但是當我每次完成一個新的功能后,重新開始整理庫的邏輯與流程,爭取每一次的代碼都足夠優(yōu)雅。同時因為我不具備第一次編寫就能通盤考慮的能力。不過希望在今后的工作中,不斷進步。這樣也能減少代碼的返工。

          其他

          函數(shù)創(chuàng)建

          事實上,我在為當前庫添加手動管理時候,考慮過直接復制函數(shù),因為函數(shù)本身是一個對象。同時為當前函數(shù)添加 set 等方法。但是沒有辦法把作用域鏈拷貝過去。

          雖然沒能成功,但是也學到了一些知識,這里也提供兩個創(chuàng)建函數(shù)的代碼。

          我們在創(chuàng)建函數(shù)時候基本上會利用 new Function 創(chuàng)建函數(shù),但是瀏覽器沒有提供可以直接創(chuàng)建異步函數(shù)的構(gòu)造器,我們需要手動獲取。

          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

          對于全局函數(shù),我們也可以直接 fn.toString() 來創(chuàng)建函數(shù),這時候異步函數(shù)也可以直接構(gòu)造的。

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

          參考資料

          前端 api 請求緩存方案[8]

          ECMAScript 6 入門 代理篇[9]

          memoizee[10]

          memoizee-proxy[11]

          參考資料


          [1]

          前端 api 請求緩存方案: https://github.com/wsafight/personBlog/issues/2

          [2]

          ECMAScript 6 入門 代理篇: https://es6.ruanyifeng.com/#docs/proxy

          [3]

          Reflect: https://es6.ruanyifeng.com/#docs/reflect

          [4]

          memoizee: https://github.com/medikoo/memoizee

          [5]

          memoizee-proxy: https://github.com/wsafight/memoizee-proxy

          [6]

          JEST: https://www.jestjs.cn/

          [7]

          proxy-pollfill: https://github.com/GoogleChrome/proxy-polyfill

          [8]

          前端 api 請求緩存方案: https://github.com/wsafight/personBlog/issues/2

          [9]

          ECMAScript 6 入門 代理篇: https://es6.ruanyifeng.com/#docs/proxy

          [10]

          memoizee: https://github.com/medikoo/memoizee

          [11]

          memoizee-proxy: https://github.com/wsafight/memoizee-proxy

          瀏覽 45
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  丁香五月婷婷无码 | 大美女大香蕉网页 | 亚洲区免费 | 黄色三级片在线啊不要 | 欧洲亚洲日本在线 |