系列文章:javascript
昨天阅读 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
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()
转化为字符串后进行处理!刚看到的我是惊呆了的 —— 之前只听有人开玩笑这么干,没想到真会这么作。数据结构
这种方法十分简单,并且可读性很高,可是会存在问题:异步
当对象结构复杂时,JSON.stringify()
会消耗很多时间。
对于不一样的正则对象,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;
}
复制代码
总体逻辑十分清晰,主要是完成两个动做:
Map
的 retCache
做为函数执行结果的缓存,缓存的键值为 defaultCacheKey
哈希后的结果。WeakMap
的 cacheStore
做为总体的缓存,缓存的键值为函数自己。经过上面两个动做造成的二级缓存实现了模块的核心功能,这里两个类型的选择很是值得探究。
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 的博客 欢迎来访 ^_^