天天閱讀一個 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 的博客 歡迎來訪 ^_^

相關文章
相關標籤/搜索