天天阅读一个 npm 模块(2)- mem

系列文章:javascript

  1. 天天阅读一个 npm 模块(1)- username

昨天阅读 username 3.0.0 版本的源码以后,根据本身的想法向做者 Sindre Sorhus 提出了 Pull Request,没想到今天 Sindre 接受了 PR 同时放弃了对 Node 4 的支持,升级至 4.0.0 版本,不过核心代码并有太大的变化 😊java

一句话介绍

今天阅读的 npm 模块是 mem,它经过缓存函数的返回值从而减小函数的实际执行次数,进而提高性能,当前版本为 3.0.1,周下载量约为 350 万。git

用法

const mem = require('mem');
 
// 同步函数缓存
let i = 0;
const counter = () => ++i;
const memoized = mem(counter);
 
memoized('foo');
//=> 1
 
memoized('foo');
//=> 1 参数相同,返回换成的结果 1
 
memoized('bar');
//=> 2 参数变化,counter 函数再次执行,返回 2
 
memoized('bar');
//=> 2

// 异步函数缓存
let j = 0;
const asyncCounter = () => Promise.resolve(++j);
const asyncmemoized = mem(asyncCounter);

asyncmemoized().then(a => {
    console.log(a);
    //=> 1
 
    asyncmemoized().then(b => {
        console.log(b);
        //=> 1
    });
});
复制代码

上述用法是 mem 的核心功能,除此以外它还支持 设置缓存时间、自定义缓存 Hash 值、统计缓存命中数据等功能。github

源码学习

哈希函数

为了让被 mem 处理过的函数对于相同的参数能返回一样的值,那么就必须对参数进行哈希处理,而后将哈希结果做为 key,函数运行结果做为 value 缓存起来,举一个最简单的例子:面试

const cache = {};

// 缓存 arg1 的运行结果
const key1 = getHash(arg1);
cache[key1] = func(arg1);

// 缓存 arg2 的运行结果
const key2 = getHash(arg2);
cache[key2] = func(arg2);
复制代码

其中的关键在于 getHash 这个哈希函数:如何处理不一样的数据类型?如何处理对象间的比较?其实这也是面试中常常被问到的问题:如何进行深比较?来看看源代码中是怎么写的:npm

// 源代码 2-1: mem 的哈希函数
const defaultCacheKey = (...args) => {
	if (args.length === 1) {
		const [firstArgument] = args;
		if (
			firstArgument === null ||
			firstArgument === undefined ||
			(typeof firstArgument !== 'function' && typeof firstArgument !== 'object')
		) {
			return firstArgument;
		}
	}

	return JSON.stringify(args);
};
复制代码

从上面的代码中能够看到:segmentfault

  1. 当只有一个参数,且参数为 null | undefined 或者类型不为 function | object 时,哈希函数直接将参数返回。
  2. 若不是上述状况,则返回参数通过 JSON.stringify() 的值。

首先能够复习一下 ES6 中定义了其中数据类型,包括 6 种原始类型(Boolean | Nunber | Null | Undefined | String| Symbol)和 Object 类型。源代码中的哈希函数须要对不一样的类型加以区分是由于 Object 类型的直接比较结果和咱们这里须要达成的效果不符合:缓存

const object1 = {a: 1};
const object2 = {a: 1};

console.log(object1 === object2);
// => flase

// 指望效果
console.log(defaultCacheKey(object1) === defaultCacheKey(object2));
// => true
复制代码

一开始我觉得做者会经过判断不一样的数据类型后再进行专门的处理(相似于 Lodash 的 _.isEqual() 实现),没想到采用的方法这么暴力:直接将 Object 类型的数据经过 JSON.stringify() 转化为字符串后进行处理!刚看到的我是惊呆了的 —— 之前只听有人开玩笑这么干,没想到真会这么作。数据结构

这种方法十分简单,并且可读性很高,可是会存在问题:异步

  1. 当对象结构复杂时,JSON.stringify() 会消耗很多时间。

  2. 对于不一样的正则对象,JSON.stringify() 的结果均为 {},与哈希函数的预期效果不符。

    console.log(JSON.stringify(/Sindre Sorhus/));
    // => '{}'
    
    console.log(JSON.stringify(/Elvin Peng/));
    // => '{}'
    复制代码

第一个问题还好,由于假如经过 JSON.stringify() 哈希时,性能存在问题的话,mem 支持传入自定义的哈希函数,能够经过自行编写高效哈希函数进行解决。

第二个问题属于函数功能不符合预期,须要进行 bugfix。

存储结构

不考虑额外参数时,对于同步函数的支持源代码可简化以下:

// 源代码 2-2 mem 核心逻辑
const mimicFn = require('mimic-fn');

const cacheStore = new WeakMap();

module.exports = (fn) => {
    const memoized = function (...args) {
        const cache = cacheStore.get(memoized);
        const key = defaultCacheKey(...args);
        
        if (cache.has(key)) {
            const c = cache.get(key);
            return c.data;
        }

        const ret = fn.call(this, ...args);
        
        const setData = (key, data) => {
            cache.set(key, {
                data,
            });
        };
        
        setData(key, ret);
        
        return ret;
    }
    
    const retCache = new Map();
   
    mimicFn(memoized, fn);

    cacheStore.set(memoized, retCache);

    return memoized;
}

复制代码

总体逻辑十分清晰,主要是完成两个动做:

  1. 将类型为 MapretCache 做为函数执行结果的缓存,缓存的键值为 defaultCacheKey 哈希后的结果。
  2. 将类型为 WeakMapcacheStore 做为总体的缓存,缓存的键值为函数自己。

经过上面两个动做造成的二级缓存实现了模块的核心功能,这里两个类型的选择很是值得探究。

retCache 选用 Map 类型而不用 Object 类型主要是由于 Map 的键值支持全部类型,而 Object 的键值只支持字符串,除此以外,关于缓存数据结构优选选择 Map 类型还有如下优势:

  • Map.size 属性能够方便的得到当前缓存的个数
  • Map 类型支持 clear() | forEach() 等经常使用的工具函数
  • Map 类型是默承认迭代的,即支持 iterable protocol

cacheStore 选用 WeakMap 类型而不用 Map 类型主要是由于其具备不增长引用个数的优势,更有利于 Node.js 引擎的垃圾回收。

异步支持

原本还打算写一写关于异步支持的部分,不过如今已是凌晨一点,想一想仍是算了吧,早点睡觉 😪

感兴趣的朋友能够本身阅读~

写在最后

除了上文提到的一个 Bug 以外,mem 还存在内存泄漏的可能性:当缓存的数据已过时后(即被缓存的时间大于设置的 maxAge)并不会被自动清除,这可能形成当缓存的数据过多以后其无效缓存占据的内存没法被及时释放,从而致使内存泄漏,具体的讨论能够见Issue #14: Memory leak: old results are not deleted from the cache

在源代码 2-2 的解读中故意略去了 mimicFn(memoized, fn); 的做用,为何呢?由于明天准备阅读 mimicFn 这个模块,但愿你们能继续捧场。

关于我:毕业于华科,工做在腾讯,elvin 的博客 欢迎来访 ^_^

相关文章
相关标签/搜索