关于javascript:200-行代码实现一个高效缓存库

39次阅读

共计 6738 个字符,预计需要花费 17 分钟才能阅读完成。


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

一、介绍

「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,具体能够从文档中获取,比拟罕用的如 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 类图如下:

在第 ① 步实例化时,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 缓存库源码外围逻辑,其源码逻辑并不简单,次要便是反对各种缓存策略和对应的解决逻辑。文章最初和大家演绎一种通用缓存库设计方案,大家有趣味能够本人实战试试,好忘性不如烂笔头。
思路最重要,这种思路能够使用在很多场景,大家能够在理论业务中多多练习和总结。​

六、还有几点思考

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

正文完
 0