主頁 > 軟體設計 > 200 行 TypeScript 代碼實作一個高效快取庫

200 行 TypeScript 代碼實作一個高效快取庫

2021-11-02 10:37:58 軟體設計

這兩天用到 cacheables 快取庫,覺得挺不錯的,和大家分享一下我看完原始碼的總結,

一、介紹

「cacheables」正如它名字一樣,是用來做記憶體快取使用,其代碼僅僅 200 行左右(不含注釋),官方的介紹如下:
cacheable介紹

一個簡單的記憶體快取,支持不同的快取策略,使用 TypeScript 撰寫優雅的語法,

它的特點:

  • 優雅的語法,包裝現有 API 呼叫,節省 API 呼叫;
  • 完全輸入的結果,不需要型別轉換,
  • 支持不同的快取策略,
  • 集成日志:檢查 API 呼叫的時間,
  • 使用輔助函式來構建快取 key,
  • 適用于瀏覽器和 Node.js,
  • 沒有依賴,
  • 進行大范圍測驗,
  • 體積小,gzip 之后 1.43kb,

當我們業務中需要對請求等異步任務做快取,避免重復請求時,完全可以使用上「cacheables」,

二、上手體驗

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

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

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

接下來看下官網提供的快取請求的使用示例:

1. 安裝依賴

npm install cacheables
// 或者
pnpm add cacheables

2. 使用示例

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

// 創建一個新的快取實體  ①
const cache = new Cacheables({
  logTiming: true,
  log: true,
});

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

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

const start = async () => {
  // 獲取新資料,并添加到快取中
  const weatherData = await getWeatherData();

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

  // 快取新資料,maxAge設定5秒,此時還未過期
  const cachedWeatherData = await getWeatherData();

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

  // 快取超過5秒,此時已過期,此時請求的資料將會再快取起來
  const freshWeatherData = await getWeatherData();
};

start();

上面示例代碼我們就實作一個請求快取的業務,在 maxAge為 5 秒內的重復請求,不會重新發送請求,而是從快取讀取其結果進行回傳,

3. API 介紹

官方檔案中介紹了很多 API,具體可以從檔案中獲取,比較常用的如 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 專案下來后,可以看到主要邏輯都在 index.ts中,去掉換行和注釋,代碼量 200 行左右,閱讀起來比較簡單,
接下來我們按照官方提供的示例,作為主線來閱讀原始碼,

1. 創建快取實體

示例中第 ① 步中,先通過 new Cacheables()創建一個快取實體,在原始碼中Cacheables類的定義如下,這邊先刪掉多余代碼,看下類提供的方法和作用:

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

  // 洗掉一筆快取
  delete(): void {}

  // 清除所有快取
  clear(): void {}

  // 回傳指定 key 的快取物件是否存在,并且有效(即是否超時)
  isCached(key: string): boolean {}

  // 回傳所有的快取 key
  keys(): string[] {}

  // 用來包裝方法呼叫,做快取
  async cacheable<T>(): Promise<T> {}
}

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

UML1

在第 ① 步實體化時,Cacheables 內部建構式會將入參保存起來,介面定義如下:

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

export type CacheOptions = {
  // 快取開關
  enabled?: boolean;
  // 啟用/禁用快取命中日志
  log?: boolean;
  // 啟用/禁用計時
  logTiming?: boolean;
};

根據引數可以看出,此時我們 Cacheables 實體支持快取日志和計時功能,

2. 包裝快取方法

第 ② 步中,我們將請求方法包裝在 cache.cacheable方法中,實作使用 max-age作為快取策略,并且有效期 5000 毫秒的快取:

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

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

// 執行快取設定
async cacheable<T>(
  resource: () => Promise<T>,  // 一個回傳Promise的函式
  key: string,  // 快取的 key
  options?: CacheableOptions, // 快取策略
): Promise<T> {
  const shouldCache = this.enabled
  // 沒有啟用快取,則直接呼叫傳入的函式,并回傳呼叫結果
  if (!shouldCache) {
    return resource()
  }
	// ... 省略日志代碼
  const result = await this.#cacheable(resource, key, options) // 核心
	// ... 省略日志代碼
  return result
}

其中cacheable 方法接收三個引數:

  • resource:需要包裝的函式,是一個回傳 Promise 的函式,如 () => fetch()
  • key:用來做快取的 key
  • options:快取策略的配置選項;

?

回傳 this.#cacheable私有方法執行的結果,this.#cacheable私有方法實作如下:

// 處理快取,如保存快取物件等
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
  }
	// 呼叫對應快取策略
  return await cacheable.touch(resource, options)
}

this.#cacheable私有方法接收的引數與 cacheable方法一樣,回傳的是 cacheable.touch方法呼叫的結果,
如果 key 的快取物件不存在,則通過 Cacheable類創建一個,其 UML 類圖如下:
UML2

3. 處理快取策略

上一步中,會通過呼叫 cacheable.touch方法,來執行對應快取策略,該方法定義如下:

// 執行快取策略的方法
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 選擇對應策略進行處理
  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方法接收兩個引數,來自 #cacheable私有方法引數的 resourceoptions
本案例使用的是 max-age快取策略,所以我們看看對應的 #handleMaxAge私有方法定義(其他的類似):

// maxAge 快取策略的處理方法
#handleMaxAge(resource: () => Promise<T>, maxAge: number) {
	// #lastFetch 最后發送時間,在 fetch 時會記錄當前時間
	// 如果當前時間大于 #lastFetch + maxAge 時,會非并發呼叫傳入的方法
  if (!this.#lastFetch || Date.now() > this.#lastFetch + maxAge) {
    return this.#fetchNonConcurrent(resource)
  }
  return this.#value // 如果是快取期間,則直接回傳前面快取的結果
}

當我們第二次執行 getWeatherData() 已經是 6 秒后,已經超過 maxAge設定的 5 秒,所有之后就會快取失效,重新發請求,
?

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

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

#fetchNonConcurrent私有方法只接收引數 resource,即需要包裝的函式,
?
這邊先判斷當前是否是【發送中】狀態,如果則直接呼叫 this.#promise,并回傳快取的值,結束呼叫,否則將 resource 傳入 #fetch執行,

#fetch私有方法定義如下:

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

#fetch 私有方法接收前面的需要包裝的函式,并通過對守衛變數賦值,控制任務的執行,在剛開始執行時進行賦值,任務執行完成以后,清空守衛變數,
?
這也是我們實際業務開發經常用到的方法,比如發請求前,通過一個變數賦值,表示當前有任務執行,不能在發其他請求,在請求結束后,將該變數清空,繼續執行其他任務,
?
完成任務,「cacheables」執行程序大致是這樣,接下來我們總結一個通用的快取方案,便于理解和拓展,

四、通用快取庫設計方案

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

快取策略

這里總結一套通用快取庫設計方案,大致如下圖:

通用方案

該快取庫支持實體化是傳入 options引數,將用戶傳入的 options.key作為 key,呼叫CachePolicyHandler物件中獲取用戶指定的快取策略(Cache Policy),
然后將用戶傳入的 options.resource作為實際要執行的方法,通過 CachePlicyHandler()方法傳入并執行,
?
上圖中,我們需要定義各種快取庫操作方法(如讀取、設定快取的方法)和各種快取策略的處理方法,
?
當然也可以集成如 Logger等輔助工具,方便用戶使用和開發,本文就不在贅述,核心還是介紹這個方案,

五、總結

本文與大家分享 cacheables 快取庫原始碼核心邏輯,其原始碼邏輯并不復雜,主要便是支持各種快取策略和對應的處理邏輯,文章最后和大家歸納一種通用快取庫設計方案,大家有興趣可以自己實戰試試,好記性不如爛筆頭,
思路最重要,這種思路可以運用在很多場景,大家可以在實際業務中多多練習和總結,?

六、還有幾點思考

1. 思考讀原始碼的方法

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

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

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

2. 思考面向介面編程

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

  1. 功能分析:對整個需求進行分析,了解需要實作的功能和細節,通過 xmind 等工具進行梳理,避免做著做著,經常返工,并且代碼結構混亂,
  2. 功能設計:梳理完需求后,可以對每個部分進行設計,如抽取通用方法等,
  3. 功能實作:前兩步都做好,相信功能實作已經不是什么難度了~

3. 思考這個庫的優化點

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

  1. 對代碼進行拆分,將一些獨立的邏輯拆到單獨檔案維護,比如每個快取策略的邏輯,可以單獨一個檔案,通過統一開發方式開發(如 Plugin),再統一入口檔案匯入和匯出,
  2. 可以將 Logger這類內部工具方法改造成支持用戶自定義,比如可以使用其他日志工具方法,不一定使用內置 Logger,更加解耦,可以參考插件化架構設計,這樣這個庫會更加靈活可拓展,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/344316.html

標籤:其他

上一篇:第三屆全國大學生演算法設計與編程挑戰賽個人銀首

下一篇:一萬粉的時候,我爬光了我所有的粉絲,只為驗證一個事情

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more