Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a simple FIFO cache to defaultMemoize, with a cacheSize option #238

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ Takes one or more selectors, or an array of selectors, computes their values and
`createSelector` determines if the value returned by an input-selector has changed between calls using reference equality (`===`). Inputs to selectors created with `createSelector` should be immutable.

Selectors created with `createSelector` have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector.
Most of the time this is all you need, but you can use `createSelectorWithCacheSize` to set a larger cache size. You can also create [a custom selector creator]((#customize-equalitycheck-for-defaultmemoize) to also configure the equality check.

```js
const mySelector = createSelector(
Expand Down Expand Up @@ -457,11 +458,16 @@ const totalSelector = createSelector(

```

### defaultMemoize(func, equalityCheck = defaultEqualityCheck)
### createSelectorWithCacheSize(cacheSize, ...inputSelectors | [inputSelectors], resultFunc)

Same as createSelector, but the first argument configures the cache size. The selector will store results in a [FIFO cache](https://en.wikipedia.org/wiki/Cache_replacement_policies#First_In_First_Out_.28FIFO.29), which means that it will overwrite the oldest value when the cache is full.


### defaultMemoize(func, equalityCheck = defaultEqualityCheck, cacheSize = 1)

`defaultMemoize` memoizes the function passed in the func parameter. It is the memoize function used by `createSelector`.

`defaultMemoize` has a cache size of 1. This means it always recalculates when the value of an argument changes.
`defaultMemoize` has a default cache size of 1. This means it always recalculates when the value of an argument changes. You can specify a larger cache size to store multiple results. It uses a simple [FIFO cache](https://en.wikipedia.org/wiki/Cache_replacement_policies#First_In_First_Out_.28FIFO.29), which means that it will overwrite the oldest result when the cache is full.

`defaultMemoize` determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using reference equality:

Expand All @@ -473,6 +479,7 @@ function defaultEqualityCheck(currentVal, previousVal) {

`defaultMemoize` can be used with `createSelectorCreator` to [customize the `equalityCheck` function](#customize-equalitycheck-for-defaultmemoize).


### createSelectorCreator(memoize, ...memoizeOptions)

`createSelectorCreator` can be used to make a customized version of `createSelector`.
Expand Down Expand Up @@ -504,6 +511,18 @@ customMemoize(resultFunc, option1, option2, option3)

Here are some examples of how you might use `createSelectorCreator`:

#### Customize `cacheSize` for `defaultMemoize`

```js
import { createSelectorCreator, defaultMemoize, defaultEqualityCheck } from 'reselect'

const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
defaultEqualityCheck,
5 // cache up to 5 results
)
```

#### Customize `equalityCheck` for `defaultMemoize`

```js
Expand Down Expand Up @@ -795,7 +814,7 @@ const veryExpensive = expensiveFilter(1000000)

### Q: The default memoization function is no good, can I use a different one?

A: We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize).
A: You can set the `cacheSize` option to cache more than one result. See [these examples](#customize-equalitycheck-for-defaultmemoize) for more ways to customize memoization and equality.

### Q: How do I test a selector?

Expand Down
82 changes: 71 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function defaultEqualityCheck(a, b) {
export function defaultEqualityCheck(a, b) {
return a === b
}

Expand All @@ -18,19 +18,73 @@ function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
return true
}

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
let lastArgs = null
let lastResult = null
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck, cacheSize = 1) {
if (cacheSize < 1) throw new Error('cacheSize must be greater than zero!')

let argsArr, resultsArr, resultsLength, lastIndex, endIndex, lastCacheHitIndex, i, j

const clearCache = () => {
argsArr = []
resultsArr = []
for (i = 0; i < cacheSize; i++) {
// Must set to null for the test in areArgumentsShallowlyEqual.
argsArr[i] = null
resultsArr[i] = null
}
resultsLength = 0
lastIndex = cacheSize
endIndex = cacheSize
lastCacheHitIndex = cacheSize - 1
}
clearCache()

// we reference arguments instead of spreading them for performance reasons
return function () {
if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
// apply arguments instead of spreading for performance.
lastResult = func.apply(null, arguments)
const memoizedResultFunc = function () {
// Check the most recent cache hit first
if (areArgumentsShallowlyEqual(equalityCheck, argsArr[lastCacheHitIndex], arguments)) {
argsArr[lastCacheHitIndex] = arguments
return resultsArr[lastCacheHitIndex]
}

// Search from newest to oldest, skipping the last cache hit
for (i = lastIndex; i < endIndex; i++) {
if (i === lastCacheHitIndex) continue

// Use modulus to cycle through the array
j = i % cacheSize
if (areArgumentsShallowlyEqual(equalityCheck, argsArr[j], arguments)) {
lastCacheHitIndex = j
argsArr[j] = arguments
return resultsArr[j]
}
}

if (lastIndex === 0) {
lastIndex = cacheSize - 1
} else {
if (resultsLength < cacheSize) resultsLength++
lastIndex--
}
endIndex = lastIndex + resultsLength
lastCacheHitIndex = lastIndex

lastArgs = arguments
return lastResult
// Apply arguments instead of spreading for performance.
resultsArr[lastIndex] = func.apply(null, arguments)

// Must set arguments after result, in case result func throws an error.
argsArr[lastIndex] = arguments

return resultsArr[lastIndex]
}

memoizedResultFunc.getArgsArr = () => argsArr
memoizedResultFunc.getResultsArr = () => resultsArr
memoizedResultFunc.getLastIndex = () => lastIndex
memoizedResultFunc.getLastCacheHitIndex = () => lastCacheHitIndex
memoizedResultFunc.getResultsLength = () => resultsLength
memoizedResultFunc.clearCache = clearCache

return memoizedResultFunc
}

function getDependencies(funcs) {
Expand Down Expand Up @@ -79,13 +133,19 @@ export function createSelectorCreator(memoize, ...memoizeOptions) {
})

selector.resultFunc = resultFunc
selector.memoizedResultFunc = memoizedResultFunc
if (typeof memoizedResultFunc.clearCache === 'function')
selector.clearCache = memoizedResultFunc.clearCache
selector.recomputations = () => recomputations
selector.resetRecomputations = () => recomputations = 0
selector.resetRecomputations = () => { recomputations = 0 }
return selector
}
}

export const createSelector = createSelectorCreator(defaultMemoize)
export const createSelectorWithCacheSize = (cacheSize, ...args) => (
createSelectorCreator(defaultMemoize, defaultEqualityCheck, cacheSize)
)(...args)

export function createStructuredSelector(selectors, selectorCreator = createSelector) {
if (typeof selectors !== 'object') {
Expand Down
Loading