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

          200 行代碼實現(xiàn)一個高效緩存庫

          共 10960字,需瀏覽 22分鐘

           ·

          2022-10-15 00:45

          這兩天用到 cacheables[1] 緩存庫,覺得挺不錯的,和大家分享一下我看完源碼的總結(jié)。

          一、介紹

          「cacheables」正如它名字一樣,是用來做內(nèi)存緩存使用,其代碼僅僅 200 行左右(不含注釋),官方的介紹如下:

          一個簡單的內(nèi)存緩存,支持不同的緩存策略,使用 TypeScript 編寫優(yōu)雅的語法。

          它的特點:

          • 優(yōu)雅的語法,包裝現(xiàn)有 API 調(diào)用,節(jié)省 API 調(diào)用;
          • 完全輸入的結(jié)果。不需要類型轉(zhuǎn)換。
          • 支持不同的緩存策略。
          • 集成日志:檢查 API 調(diào)用的時間。
          • 使用輔助函數(shù)來構(gòu)建緩存 key。
          • 適用于瀏覽器和 Node.js。
          • 沒有依賴。
          • 進行大范圍測試。
          • 體積小,gzip 之后 1.43kb。

          當(dāng)我們業(yè)務(wù)中需要對請求等異步任務(wù)做緩存,避免重復(fù)請求時,完全可以使用上「cacheables」。

          二、上手體驗

          上手 cacheables很簡單,看看下面使用對比:

          // 沒有使用緩存
          fetch("https://some-url.com/api");

          // 有使用緩存
          cache.cacheable(() => fetch("https://some-url.com/api"), "key");

          接下來看下官網(wǎng)提供的緩存請求的使用示例:

          1. 安裝依賴

          npm install cacheables
          // 或者
          pnpm add cacheables

          2. 使用示例

          import { Cacheables } from "cacheables";
          const apiUrl = "http://localhost:3000/";

          // 創(chuàng)建一個新的緩存實例  ①
          const cache = new Cacheables({
            logTiming: true,
            log: true,
          });

          // 模擬異步任務(wù)
          const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

          // 包裝一個現(xiàn)有 API 調(diào)用 fetch(apiUrl),并分配一個 key 為 weather
          // 下面例子使用 'max-age' 緩存策略,它會在一段時間后緩存失效
          // 該方法返回一個完整 Promise,就像' fetch(apiUrl) '一樣,可以緩存結(jié)果。
          const getWeatherData = () =>
            // ②
            cache.cacheable(() => fetch(apiUrl), "weather", {
              cachePolicy: "max-age",
              maxAge: 5000,
            });

          const start = async () => {
            // 獲取新數(shù)據(jù),并添加到緩存中
            const weatherData = await getWeatherData();

            // 3秒之后再執(zhí)行
            await wait(3000);

            // 緩存新數(shù)據(jù),maxAge設(shè)置5秒,此時還未過期
            const cachedWeatherData = await getWeatherData();

            // 3秒之后再執(zhí)行
            await wait(3000);

            // 緩存超過5秒,此時已過期,此時請求的數(shù)據(jù)將會再緩存起來
            const freshWeatherData = await getWeatherData();
          };

          start();

          上面示例代碼我們就實現(xiàn)一個請求緩存的業(yè)務(wù),在 maxAge為 5 秒內(nèi)的重復(fù)請求,不會重新發(fā)送請求,而是從緩存讀取其結(jié)果進行返回。

          3. API 介紹

          官方文檔中介紹了很多 API,具體可以從文檔[2]中獲取,比較常用的如 cache.cacheable(),用來包裝一個方法進行緩存。所有 API 如下:

          • new Cacheables(options?): Cacheables
          • cache.cacheable(resource, key, options?): Promise<T>
          • cache.delete(key: string): void
          • cache.clear(): void
          • cache.keys(): string[]
          • cache.isCached(key: string): boolean
          • Cacheables.key(...args: (string | number)[]): string

          可以通過下圖加深理解:

          三、源碼分析

          克隆 cacheables[3] 項目下來后,可以看到主要邏輯都在 index.ts中,去掉換行和注釋,代碼量 200 行左右,閱讀起來比較簡單。接下來我們按照官方提供的示例,作為主線來閱讀源碼。

          1. 創(chuàng)建緩存實例

          示例中第 ① 步中,先通過 new Cacheables()創(chuàng)建一個緩存實例,在源碼中Cacheables類的定義如下,這邊先刪掉多余代碼,看下類提供的方法和作用:

          export class Cacheables {
            constructor(options?: CacheOptions) {
              this.enabled = options?.enabled ?? true;
              this.log = options?.log ?? false;
              this.logTiming = options?.logTiming ?? false;
            }
            // 使用提供的參數(shù)創(chuàng)建一個 key
            static key(): string {}

            // 刪除一筆緩存
            delete(): void {}

            // 清除所有緩存
            clear(): void {}

            // 返回指定 key 的緩存對象是否存在,并且有效(即是否超時)
            isCached(key: string): boolean {}

            // 返回所有的緩存 key
            keys(): string[] {}

            // 用來包裝方法調(diào)用,做緩存
            async cacheable<T>(): Promise<T> {}
          }

          這樣就很直觀清楚 cacheables 實例的作用和支持的方法,其 UML 類圖如下:

          UML1

          在第 ① 步實例化時,Cacheables 內(nèi)部構(gòu)造函數(shù)會將入?yún)⒈4嫫饋恚涌诙x如下:

          const cache = new Cacheables({
            logTiming: true,
            log: true,
          });

          export type CacheOptions = {
            // 緩存開關(guān)
            enabled?: boolean;
            // 啟用/禁用緩存命中日志
            log?: boolean;
            // 啟用/禁用計時
            logTiming?: boolean;
          };

          根據(jù)參數(shù)可以看出,此時我們 Cacheables 實例支持緩存日志和計時功能。

          2. 包裝緩存方法

          第 ② 步中,我們將請求方法包裝在 cache.cacheable方法中,實現(xiàn)使用 max-age作為緩存策略,并且有效期 5000 毫秒的緩存:

          const getWeatherData = () =>
            cache.cacheable(() => fetch(apiUrl), "weather", {
              cachePolicy: "max-age",
              maxAge: 5000,
            });

          其中,cacheable 方法是 Cacheables類上的成員方法,定義如下(移除日志相關(guān)代碼):

          // 執(zhí)行緩存設(shè)置
          async cacheable<T>(
            resource: () => Promise<T>,  // 一個返回Promise的函數(shù)
            key: string,  // 緩存的 key
            options?: CacheableOptions, // 緩存策略
          ): Promise<T> {
            const shouldCache = this.enabled
            // 沒有啟用緩存,則直接調(diào)用傳入的函數(shù),并返回調(diào)用結(jié)果
            if (!shouldCache) {
              return resource()
            }
           // ... 省略日志代碼
            const result = await this.#cacheable(resource, key, options) // 核心
           // ... 省略日志代碼
            return result
          }

          其中cacheable 方法接收三個參數(shù):

          • resource:需要包裝的函數(shù),是一個返回 Promise 的函數(shù),如 () => fetch();
          • key:用來做緩存的 key
          • options:緩存策略的配置選項;


          返回 this.#cacheable私有方法執(zhí)行的結(jié)果,this.#cacheable私有方法實現(xiàn)如下:

          // 處理緩存,如保存緩存對象等
          async #cacheable<T>(
            resource: () => Promise<T>,
            key: string,
            options?: CacheableOptions,
          ): Promise<T> {
            // 先通過 key 獲取緩存對象
            let cacheable = this.#cacheables[key] as Cacheable<T> | undefined
           // 如果不存在該 key 下的緩存對象,則通過 Cacheable 實例化一個新的緩存對象
            // 并保存在該 key 下
            if (!cacheable) {
              cacheable = new Cacheable()
              this.#cacheables[key] = cacheable
            }
           // 調(diào)用對應(yīng)緩存策略
            return await cacheable.touch(resource, options)
          }

          this.#cacheable私有方法接收的參數(shù)與 cacheable方法一樣,返回的是 cacheable.touch方法調(diào)用的結(jié)果。如果 key 的緩存對象不存在,則通過 Cacheable類創(chuàng)建一個,其 UML 類圖如下:

          3. 處理緩存策略

          上一步中,會通過調(diào)用 cacheable.touch方法,來執(zhí)行對應(yīng)緩存策略,該方法定義如下:

          // 執(zhí)行緩存策略的方法
          async touch(
            resource: () => Promise<T>,
            options?: CacheableOptions,
          ): Promise<T> {
            if (!this.#initialized) {
              return this.#handlePreInit(resource, options)
            }
            if (!options) {
              return this.#handleCacheOnly()
            }
           // 通過實例化 Cacheables 時候配置的 options 的 cachePolicy 選擇對應(yīng)策略進行處理
            switch (options.cachePolicy) {
              case 'cache-only':
                return this.#handleCacheOnly()
              case 'network-only':
                return this.#handleNetworkOnly(resource)
              case 'stale-while-revalidate':
                return this.#handleSwr(resource)
              case 'max-age'// 本案例使用的類型
                return this.#handleMaxAge(resource, options.maxAge)
              case 'network-only-non-concurrent':
                return this.#handleNetworkOnlyNonConcurrent(resource)
            }
          }

          touch方法接收兩個參數(shù),來自 #cacheable私有方法參數(shù)的 resourceoptions。本案例使用的是 max-age緩存策略,所以我們看看對應(yīng)的 #handleMaxAge私有方法定義(其他的類似):

          // maxAge 緩存策略的處理方法
          #handleMaxAge(resource: () => Promise<T>, maxAge: number) {
           // #lastFetch 最后發(fā)送時間,在 fetch 時會記錄當(dāng)前時間
           // 如果當(dāng)前時間大于 #lastFetch + maxAge 時,會非并發(fā)調(diào)用傳入的方法
            if (!this.#lastFetch || Date.now() > this.#lastFetch + maxAge) {
              return this.#fetchNonConcurrent(resource)
            }
            return this.#value // 如果是緩存期間,則直接返回前面緩存的結(jié)果
          }

          當(dāng)我們第二次執(zhí)行 getWeatherData() 已經(jīng)是 6 秒后,已經(jīng)超過 maxAge設(shè)置的 5 秒,所有之后就會緩存失效,重新發(fā)請求。

          再看下 #fetchNonConcurrent私有方法定義,該方法用來發(fā)送非并發(fā)的請求:

          // 發(fā)送非并發(fā)請求
          async #fetchNonConcurrent(resource: () => Promise<T>): Promise<T> {
           // 非并發(fā)情況,如果當(dāng)前請求還在發(fā)送中,則直接執(zhí)行當(dāng)前執(zhí)行中的方法,并返回結(jié)果
            if (this.#isFetching(this.#promise)) {
              await this.#promise
              return this.#value
            }
            // 否則直接執(zhí)行傳入的方法
            return this.#fetch(resource)
          }

          #fetchNonConcurrent私有方法只接收參數(shù) resource,即需要包裝的函數(shù)。這邊先判斷當(dāng)前是否是【發(fā)送中】狀態(tài),如果則直接調(diào)用 this.#promise,并返回緩存的值,結(jié)束調(diào)用。否則將 resource 傳入 #fetch執(zhí)行。

          #fetch私有方法定義如下:

          // 執(zhí)行請求發(fā)送
          async #fetch(resource: () => Promise<T>): Promise<T> {
            this.#lastFetch = Date.now()
            this.#promise = resource() // 定義守衛(wèi)變量,表示當(dāng)前有任務(wù)在執(zhí)行
            this.#value = await this.#promise
            if (!this.#initialized) this.#initialized = true
            this.#promise = undefined  // 執(zhí)行完成,清空守衛(wèi)變量
            return this.#value
          }

          #fetch 私有方法接收前面的需要包裝的函數(shù),并通過對「守衛(wèi)變量」賦值,控制任務(wù)的執(zhí)行,在剛開始執(zhí)行時進行賦值,任務(wù)執(zhí)行完成以后,清空守衛(wèi)變量。這也是我們實際業(yè)務(wù)開發(fā)經(jīng)常用到的方法,比如發(fā)請求前,通過一個變量賦值,表示當(dāng)前有任務(wù)執(zhí)行,不能在發(fā)其他請求,在請求結(jié)束后,將該變量清空,繼續(xù)執(zhí)行其他任務(wù)。完成任務(wù)?!竎acheables」執(zhí)行過程大致是這樣,接下來我們總結(jié)一個通用的緩存方案,便于理解和拓展。

          四、通用緩存庫設(shè)計方案

          在 Cacheables 中支持五種緩存策略,上面只介紹其中的 max-age

          緩存策略

          這里總結(jié)一套通用緩存庫設(shè)計方案,大致如下圖:

          通用方案

          該緩存庫支持實例化是傳入 options參數(shù),將用戶傳入的 options.key作為 key,調(diào)用CachePolicyHandler對象中獲取用戶指定的緩存策略(Cache Policy)。然后將用戶傳入的 options.resource作為實際要執(zhí)行的方法,通過 CachePlicyHandler()方法傳入并執(zhí)行。上圖中,我們需要定義各種緩存庫操作方法(如讀取、設(shè)置緩存的方法)和各種緩存策略的處理方法。當(dāng)然也可以集成如 Logger等輔助工具,方便用戶使用和開發(fā)。本文就不在贅述,核心還是介紹這個方案。

          五、總結(jié)

          本文與大家分享 cacheables[4] 緩存庫源碼核心邏輯,其源碼邏輯并不復(fù)雜,主要便是支持各種緩存策略和對應(yīng)的處理邏輯。文章最后和大家歸納一種通用緩存庫設(shè)計方案,大家有興趣可以自己實戰(zhàn)試試,好記性不如爛筆頭。思路最重要,這種思路可以運用在很多場景,大家可以在實際業(yè)務(wù)中多多練習(xí)和總結(jié)。

          六、還有幾點思考

          1. 思考讀源碼的方法

          大家都在讀源碼,討論源碼,那如何讀源碼?個人建議:

          1. 先確定自己要學(xué)源碼的部分(如 Vue2 響應(yīng)式原理、Vue3 Ref 等);
          2. 根據(jù)要學(xué)的部分,寫個簡單 demo;
          3. 通過 demo 斷點進行大致了解;
          4. 翻閱源碼,詳細閱讀,因為源碼中往往會有注釋和示例等。

          如果你只是單純想開始學(xué)某個庫,可以先閱讀 README.md,重點開介紹、特點、使用方法、示例等。抓住其特點、示例進行針對性的源碼閱讀。相信這樣閱讀起來,思路會更清晰。

          2. 思考面向接口編程

          這個庫使用了 TypeScript,通過每個接口定義,我們能很清晰的知道每個類、方法、屬性作用。這也是我們需要學(xué)習(xí)的。在我們接到需求任務(wù)時,可以這樣做,你的效率往往會提高很多:

          1. 「功能分析」:對整個需求進行分析,了解需要實現(xiàn)的功能和細節(jié),通過 xmind 等工具進行梳理,避免做著做著,經(jīng)常返工,并且代碼結(jié)構(gòu)混亂。
          2. 「功能設(shè)計」:梳理完需求后,可以對每個部分進行設(shè)計,如抽取通用方法等,
          3. 「功能實現(xiàn)」:前兩步都做好,相信功能實現(xiàn)已經(jīng)不是什么難度了~

          3. 思考這個庫的優(yōu)化點

          這個庫代碼主要集中在 index.ts中,閱讀起來還好,當(dāng)代碼量增多后,恐怕閱讀體驗比較不好。所以我的建議是:

          1. 對代碼進行拆分,將一些獨立的邏輯拆到單獨文件維護,比如每個緩存策略的邏輯,可以單獨一個文件,通過統(tǒng)一開發(fā)方式開發(fā)(如 Plugin),再統(tǒng)一入口文件導(dǎo)入和導(dǎo)出。
          2. 可以將 Logger這類內(nèi)部工具方法改造成支持用戶自定義,比如可以使用其他日志工具方法,不一定使用內(nèi)置 Logger,更加解耦??梢詤⒖疾寮軜?gòu)設(shè)計,這樣這個庫會更加靈活可拓展。

          參考資料

          [1]

          cacheables: https://github.com/grischaerbe/cacheables

          [2]

          文檔: https://github.com/grischaerbe/cacheables

          [3]

          cacheables: https://github.com/grischaerbe/cacheables

          [4]

          cacheables: https://github.com/grischaerbe/cacheables

          w3cschool編程獅

          專門幫助零基礎(chǔ)的同學(xué)們學(xué)習(xí)編程基礎(chǔ)的學(xué)習(xí)

          瀏覽 37
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产一卡二卡在线看 | 婷婷爱综合| 一区二区三区四区免费观看 | 亚洲成人AV在线播放 | www.8x8x |