1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        200 行代碼實(shí)現(xiàn)一個(gè)高效緩存庫(kù)

        共 10960字,需瀏覽 22分鐘

         ·

        2022-10-15 00:45

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

        一、介紹

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

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

        它的特點(diǎn):

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

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

        二、上手體驗(yàn)

        上手 cacheables很簡(jiǎn)單,看看下面使用對(duì)比:

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

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

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

        1. 安裝依賴

        npm install cacheables
        // 或者
        pnpm add cacheables

        2. 使用示例

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

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

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

        // 包裝一個(gè)現(xiàn)有 API 調(diào)用 fetch(apiUrl),并分配一個(gè) key 為 weather
        // 下面例子使用 'max-age' 緩存策略,它會(huì)在一段時(shí)間后緩存失效
        // 該方法返回一個(gè)完整 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秒,此時(shí)還未過期
          const cachedWeatherData = await getWeatherData();

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

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

        start();

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

        3. API 介紹

        官方文檔中介紹了很多 API,具體可以從文檔[2]中獲取,比較常用的如 cache.cacheable(),用來包裝一個(gè)方法進(jìn)行緩存。所有 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] 項(xiàng)目下來后,可以看到主要邏輯都在 index.ts中,去掉換行和注釋,代碼量 200 行左右,閱讀起來比較簡(jiǎn)單。接下來我們按照官方提供的示例,作為主線來閱讀源碼。

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

        示例中第 ① 步中,先通過 new Cacheables()創(chuàng)建一個(gè)緩存實(shí)例,在源碼中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)建一個(gè) key
          static key(): string {}

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

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

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

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

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

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

        UML1

        在第 ① 步實(shí)例化時(shí),Cacheables 內(nèi)部構(gòu)造函數(shù)會(huì)將入?yún)⒈4嫫饋?,接口定義如下:

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

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

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

        2. 包裝緩存方法

        第 ② 步中,我們將請(qǐng)求方法包裝在 cache.cacheable方法中,實(shí)現(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>,  // 一個(gè)返回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 方法接收三個(gè)參數(shù):

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


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

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

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

        3. 處理緩存策略

        上一步中,會(huì)通過調(diào)用 cacheable.touch方法,來執(zhí)行對(duì)應(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()
          }
         // 通過實(shí)例化 Cacheables 時(shí)候配置的 options 的 cachePolicy 選擇對(duì)應(yīng)策略進(jìn)行處理
          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方法接收兩個(gè)參數(shù),來自 #cacheable私有方法參數(shù)的 resourceoptions。本案例使用的是 max-age緩存策略,所以我們看看對(duì)應(yīng)的 #handleMaxAge私有方法定義(其他的類似):

        // maxAge 緩存策略的處理方法
        #handleMaxAge(resource: () => Promise<T>, maxAge: number) {
         // #lastFetch 最后發(fā)送時(shí)間,在 fetch 時(shí)會(huì)記錄當(dāng)前時(shí)間
         // 如果當(dāng)前時(shí)間大于 #lastFetch + maxAge 時(shí),會(huì)非并發(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 秒,所有之后就會(huì)緩存失效,重新發(fā)請(qǐng)求。

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

        // 發(fā)送非并發(fā)請(qǐng)求
        async #fetchNonConcurrent(resource: () => Promise<T>): Promise<T> {
         // 非并發(fā)情況,如果當(dāng)前請(qǐ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í)行請(qǐng)求發(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ù),并通過對(duì)「守衛(wèi)變量」賦值,控制任務(wù)的執(zhí)行,在剛開始執(zhí)行時(shí)進(jìn)行賦值,任務(wù)執(zhí)行完成以后,清空守衛(wèi)變量。這也是我們實(shí)際業(yè)務(wù)開發(fā)經(jīng)常用到的方法,比如發(fā)請(qǐng)求前,通過一個(gè)變量賦值,表示當(dāng)前有任務(wù)執(zhí)行,不能在發(fā)其他請(qǐng)求,在請(qǐng)求結(jié)束后,將該變量清空,繼續(xù)執(zhí)行其他任務(wù)。完成任務(wù)。「cacheables」執(zhí)行過程大致是這樣,接下來我們總結(jié)一個(gè)通用的緩存方案,便于理解和拓展。

        四、通用緩存庫(kù)設(shè)計(jì)方案

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

        緩存策略

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

        通用方案

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

        五、總結(jié)

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

        六、還有幾點(diǎn)思考

        1. 思考讀源碼的方法

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

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

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

        2. 思考面向接口編程

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

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

        3. 思考這個(gè)庫(kù)的優(yōu)化點(diǎn)

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

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

        參考資料

        [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
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产美女视频一区二区三区 | 大鸡巴网免费视频在线 | 麻豆入口国产精品 | 少妇一级婬片免费 | 伊人网在线播放 | 亚洲AV无码成人精品一区 | 中文字幕av一区二区三区谷原希美 | 国产在线精品无码 | 91久色蝌蚪| 肉大棒一进一出免费视频 |