【若川视野 x 源码共读】第29期 | quick-lru

209 阅读4分钟

阅读quick-lru源码,了解其原理

LRU

LRU全称为Least Recently Used,也就是最近最少使用的意思,是一种缓存算法策略。

参考:LRU算法 - github

原理

quick-lru内部结构是两个cache(Map数据结构)作为存储,元素内容添加时间戳,设置最大时间限制、最大数量;通过以上几个变量之间的交互来实现添加、修改、删除、遍历等操作。

实现

这里我们从index.d.ts切入,通过查看基本API类型信息,来分析其内部实现原理。

construtor

constructor(options = {}) {
        super();

        if (!(options.maxSize && options.maxSize > 0)) {
                throw new TypeError('`maxSize` must be a number greater than 0');
        }

        if (typeof options.maxAge === 'number' && options.maxAge === 0) {
                throw new TypeError('`maxAge` must be a number greater than 0');
        }

        // TODO: Use private class fields when ESLint supports them.
        this.maxSize = options.maxSize;	// 最大数量
        this.maxAge = options.maxAge || Number.POSITIVE_INFINITY;	// 最长时间
        this.onEviction = options.onEviction;	// 元素从缓存中移除时调用
        this.cache = new Map();
        this.oldCache = new Map();
        this._size = 0;	// 数量大小
}

set

该api主要用于添加元素,内部通过_set来设置cache、oldcache

set(key, value, {maxAge = this.maxAge} = {}) {
  const expiry =
        typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY ?
        Date.now() + maxAge :
  undefined;  // 获取日期
  if (this.cache.has(key)) {
    // 若存在 cache中,则更新设置值
    this.cache.set(key, {
      value,
      expiry
    });
  } else {
    this._set(key, {value, expiry});
  }
}

_set(key, value) {
  this.cache.set(key, value);	// 添加元素
  this._size++;	// 元素数量加1

  if (this._size >= this.maxSize) {
    // 超出数量限制,cache元素赋值到oldCache中,cache重新赋值空map
    this._size = 0;
    // 超出数目,遍历oldCache中的元素并触发onEviction
    this._emitEvictions(this.oldCache);
    // oldCache赋值为cache
    this.oldCache = this.cache;
    // cache赋值为空值
    this.cache = new Map();
  }
}

delete

map.delete:如果 Map 对象中存在该元素,则移除它并返回 true;否则如果该元素不存在则返回 false

delete(key) {
  // cache移除元素
  const deleted = this.cache.delete(key);	
  // 若key被移除,当前元素数量减1
  if (deleted) {
    this._size--;
  }
	// oldCache中也要移除key对应的元素
  return this.oldCache.delete(key) || deleted;
}

has

判断元素是否存在cache、oldCache,是否已经过期,如已过期,通过_deleteIfExpired进行移除操作

has(key) {
  if (this.cache.has(key)) {
    // 取反的原因:若key因过期被移除,应当视为当前不存在该key。
    return !this._deleteIfExpired(key, this.cache.get(key));
  }

  if (this.oldCache.has(key)) {
    // 原因同上
    return !this._deleteIfExpired(key, this.oldCache.get(key));
  }

  return false;
}	

_deleteIfExpired(key, item) {
  if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
    // 若元素过期,则需从cache中移除该key
    if (typeof this.onEviction === 'function') {
      // key移除,触发 移除回调
      this.onEviction(key, item.value);
    }
		// 移除元素
    return this.delete(key);
  }

  return false;
}

get

根据key获取值,内部需要先通过has来进行是否过期判断;oldCache中旧值被使用,应当移到cache中

get(key) {
  if (this.cache.has(key)) {
    const item = this.cache.get(key);

    return this._getItemValue(key, item);
  }

  if (this.oldCache.has(key)) {
    const item = this.oldCache.get(key);
    if (this._deleteIfExpired(key, item) === false) {
      // 上述cache中没找到,oldCache中找到,需将oldCache的值移到cache中
      this._moveToRecent(key, item);
      return item.value;
    }
  }
}

_getItemValue(key, item) {
  // 有时间戳,需要判断是否需要移除,没时间戳,直接返回即可
  return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value;
}

_getOrDeleteIfExpired(key, item) {
  const deleted = this._deleteIfExpired(key, item);
  if (deleted === false) {
    // 不需移除,直接返回该值
    return item.value;
  }
}

_moveToRecent(key, item) {
  // 将 key 从oldCache中移除
  this.oldCache.delete(key);
  // 将key-item设置到cache中
  this._set(key, item);
}

peek

根据key获取值,不同于get的点,这里并不会将oldCache中的值移到cache中

peek(key) {
  if (this.cache.has(key)) {
    return this._peek(key, this.cache);
  }

  if (this.oldCache.has(key)) {
    return this._peek(key, this.oldCache);
  }
}

_peek(key, cache) {
  const item = cache.get(key);

  return this._getItemValue(key, item);
}

clear

将cache、oldCache中的值清空

clear() {
  this.cache.clear();
  this.oldCache.clear();
  // 元素数量置为0
  this._size = 0;
}

resize

调整cache、oldCache中元素数量

resize(newSize) {
  if (!(newSize && newSize > 0)) {
    throw new TypeError('`maxSize` must be a number greater than 0');
  }
	// 倒叙获取元素
  const items = [...this._entriesAscending()];
  const removeCount = items.length - newSize;
  if (removeCount < 0) {
    // newSize比原先size大,全部数据都置为最新值,cache置为全部值,oldCache清空,当前size更新
    this.cache = new Map(items);
    this.oldCache = new Map();
    this._size = items.length;
  } else {
    // newSize比原先小
    if (removeCount > 0) {
      // removeCount 个元素需要移除,触发元素清除回调
      this._emitEvictions(items.slice(0, removeCount));
    }
		// 移除 removeCount个元素,并重新赋值给 oldCache
    this.oldCache = new Map(items.slice(removeCount));
    // cache重新赋值
    this.cache = new Map();
    // 当前元素统计置为 0 ,
    this._size = 0;
  }

  this.maxSize = newSize;
}

// 返回迭代器对象,顺序为oldCache -> cache
* _entriesAscending() {
  for (const item of this.oldCache) {
    const [key, value] = item;
    if (!this.cache.has(key)) {
      const deleted = this._deleteIfExpired(key, value);
      if (deleted === false) {
        yield item;
      }
    }
  }

  for (const item of this.cache) {
    const [key, value] = item;
    const deleted = this._deleteIfExpired(key, value);
    if (deleted === false) {
      yield item;
    }
  }
}
// cache中的元素都触发 onEviction 函数
_emitEvictions(cache) {
  if (typeof this.onEviction !== 'function') {
    return;
  }

  for (const [key, item] of cache) {
    this.onEviction(key, item.value);
  }
}

Symbol.iterator

quick-lru中重写该默认迭代器对象,供for...of 使用。参考链接:Symbol.iterator

* [Symbol.iterator]() {
  // 重写为读取 cache -> oldCache 的顺序,并返回 [key, value] 的结构
  for (const item of this.cache) {
    const [key, value] = item;
    const deleted = this._deleteIfExpired(key, value);
    if (deleted === false) {
      yield [key, value.value];
    }
  }

  for (const item of this.oldCache) {
    const [key, value] = item;
    if (!this.cache.has(key)) {
      const deleted = this._deleteIfExpired(key, value);
      if (deleted === false) {
        yield [key, value.value];
      }
    }
  }
}

keys

* keys() {
  for (const [key] of this) {
    // 这里用调用上述 Symbol.iterator
    yield key;
  }
}

symbol-iterator.png

values

* values() {
  for (const [, value] of this) {
    // 这里用调用上述 Symbol.iterator
    yield value;
  }
}

知识补给站

Generator

  • 概念:生成器对象,返回值数据类型为Generator迭代器对象
  • 例子
    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    let g = gen();
    console.log(g);	// Generator对象
    // 获取值需通过 next 函数来操作
    console.log(g.next());	// { "value": 1, "done": false }
    console.log(g.next());	// { "value": 2, "done": false }
    console.log(g.next());	// { "value": 3, "done": false }
    console.log(g.next());	// { "value": undefined, "done": true }
    

总结

通过这次的源码共读,学习到的几点:

  • 初步了解到什么是LRU算法,以及如何简单实现
  • 了解到生成器Generator的一些基础知识
  • 如何去重写默认的Symbol.iterator,输出自己想要的信息