200 行代码实现一个高效缓存库

发表于 2年以前  | 总阅读数:334 次

这两天用到 cacheables[1] 缓存库,觉得挺不错的,和大家分享一下我看完源码的总结。

一、介绍

「cacheables」正如它名字一样,是用来做内存缓存使用,其代码仅仅 200 行左右(不含注释),官方的介绍如下:

一个简单的内存缓存,支持不同的缓存策略,使用 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,具体可以从文档[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. 创建缓存实例

示例中第 ① 步中,先通过 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 类图如下:

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[4] 缓存库源码核心逻辑,其源码逻辑并不复杂,主要便是支持各种缓存策略和对应的处理逻辑。文章最后和大家归纳一种通用缓存库设计方案,大家有兴趣可以自己实战试试,好记性不如烂笔头。思路最重要,这种思路可以运用在很多场景,大家可以在实际业务中多多练习和总结。

六、还有几点思考

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,更加解耦。可以参考插件化架构设计,这样这个库会更加灵活可拓展。

参考资料

[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

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/c6iav1412ZLPwbhsn7MV8w

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录