diff --git a/src/utils/memoize.js b/src/utils/memoize.js index 8908446b6..e7b3bb9d7 100644 --- a/src/utils/memoize.js +++ b/src/utils/memoize.js @@ -1,10 +1,8 @@ -import { Map } from 'immutable' - /** - * The leaf node of a cache tree. + * The leaf node of a cache tree. Used to support variable argument length. * - * An object, so that immutable maps will key it by reference. + * A unique object, so that native Maps will key it by reference. * * @type {Object} */ @@ -12,12 +10,19 @@ import { Map } from 'immutable' const LEAF = {} /** - * An unique value used to detect cache misses + * A value to represent a memoized undefined value. Allows efficient + * value retrieval using Map.get only. * * @type {Object} */ -const NO_SET = {} +const UNDEFINED = {} + +/** + * Default value for unset keys in native Maps + */ + +const UNSET = undefined /** * Memoize all of the `properties` on a `object`. @@ -36,23 +41,78 @@ function memoize(object, properties) { } object[property] = function (...args) { - const keys = [property, ...args, LEAF] + const keys = [property, ...args] this.__cache = this.__cache || new Map() - const cachedValue = this.__cache.getIn(keys, NO_SET) - if (cachedValue !== NO_SET) { - return cachedValue + const cachedValue = getIn(this.__cache, keys) + if (cachedValue !== UNSET) { + return cachedValue === UNDEFINED ? undefined : cachedValue } const value = original.apply(this, args) - // If `original` is recursive, it might have changed the cache, - // so read it from this.__cache - this.__cache = this.__cache.setIn(keys, value) + this.__cache = setIn(this.__cache, keys, value) return value } } } +/** + * Set a value at a key path in a tree of Map, creating Maps on the go. + * + * @param{Map} map + * @param{Array} keys + * @param{Any} value + * @return {Map} + */ + +function setIn(map, keys, value) { + value = value === undefined ? UNDEFINED : value + + let parentMap = map + let childMap + for (const key of keys) { + childMap = parentMap.get(key) + + if (childMap === UNSET) { + // This path was not created yet + childMap = new Map() + parentMap.set(key, childMap) + } + + parentMap = childMap + } + // The whole map path was created + + // Set the value to the bottom most map + childMap.set(LEAF, value) + + return map +} + +/** + * Get a value at a key path in a tree of Map. + * If not set, returns UNSET. + * If the set value is undefined, returns UNDEFINED + * + * @param{Map} map + * @param{Array} keys + * @return {Any | UNSET | UNDEFINED} + */ + +function getIn(map, keys) { + let childMap + for (const key of keys) { + childMap = map.get(key) + if (childMap === UNSET) { + // Not found + return UNSET + } + map = childMap + } + + return childMap.get(LEAF) +} + /** * Export. */