-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[core] Replace reselect with local memoizers in x-internals #22721
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export { lruMemoize } from 'reselect'; | ||
| export { lruMemoize } from './lruMemoize'; | ||
| export type { LruMemoizeOptions } from './lruMemoize'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| type EqualityFn = (a: any, b: any) => boolean; | ||
|
|
||
| export interface LruMemoizeOptions { | ||
| /** | ||
| * Function used to compare each new argument against the cached one. | ||
| * @default (a, b) => a === b | ||
| */ | ||
| equalityCheck?: EqualityFn; | ||
| /** | ||
| * When provided and a new result is computed, the cache is searched for a result | ||
| * considered equal by this function. If found, the cached value is returned instead, | ||
| * preserving referential stability. | ||
| */ | ||
| resultEqualityCheck?: EqualityFn; | ||
| /** | ||
| * The number of previous calls to keep in the cache. | ||
| * @default 1 | ||
| */ | ||
| maxSize?: number; | ||
| } | ||
|
|
||
| interface CacheEntry { | ||
| args: any[]; | ||
| value: any; | ||
| } | ||
|
|
||
| const referenceEqualityCheck: EqualityFn = (a, b) => a === b; | ||
|
|
||
| /** | ||
| * Memoizes a function using a least-recently-used cache. | ||
| * The arguments of the last `maxSize` calls are compared with the provided `equalityCheck`. | ||
| * | ||
| * Drop-in replacement for the subset of `lruMemoize` from `reselect` that MUI X relies on. | ||
| */ | ||
| export function lruMemoize<F extends (...args: any[]) => any>( | ||
| func: F, | ||
| equalityCheckOrOptions?: EqualityFn | LruMemoizeOptions, | ||
| ): F { | ||
| const options: LruMemoizeOptions = | ||
| typeof equalityCheckOrOptions === 'object' | ||
| ? equalityCheckOrOptions | ||
| : { equalityCheck: equalityCheckOrOptions }; | ||
| const { equalityCheck = referenceEqualityCheck, maxSize = 1, resultEqualityCheck } = options; | ||
|
|
||
| let entries: CacheEntry[] = []; | ||
|
|
||
| const areArgsEqual = (cachedArgs: any[], args: any[]) => { | ||
| if (cachedArgs.length !== args.length) { | ||
| return false; | ||
| } | ||
| for (let i = 0; i < args.length; i += 1) { | ||
| if (!equalityCheck(cachedArgs[i], args[i])) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| }; | ||
|
|
||
| function memoized(...args: any[]) { | ||
| for (let i = 0; i < entries.length; i += 1) { | ||
| const entry = entries[i]; | ||
| if (areArgsEqual(entry.args, args)) { | ||
| if (i > 0) { | ||
| // Move the hit to the front of the cache | ||
| entries.splice(i, 1); | ||
| entries.unshift(entry); | ||
| } | ||
| return entry.value; | ||
| } | ||
| } | ||
|
|
||
| let value = func(...args); | ||
| if (resultEqualityCheck) { | ||
| const matchingEntry = entries.find((entry) => resultEqualityCheck(entry.value, value)); | ||
| if (matchingEntry) { | ||
| value = matchingEntry.value; | ||
| } | ||
| } | ||
|
|
||
| entries.unshift({ args, value }); | ||
| if (entries.length > maxSize) { | ||
| entries.pop(); | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| memoized.clearCache = () => { | ||
| entries = []; | ||
| }; | ||
|
|
||
| return memoized as unknown as F; | ||
| } |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Although considering the popularity of Redux Toolkit, some of our users might see a net loss because they'd be shipping both our code and reselect's code. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| interface CacheNode { | ||
| computed: boolean; | ||
| value: unknown; | ||
| objects: WeakMap<object, CacheNode> | null; | ||
| primitives: Map<unknown, CacheNode> | null; | ||
| } | ||
|
|
||
| const createCacheNode = (): CacheNode => ({ | ||
| computed: false, | ||
| value: undefined, | ||
| objects: null, | ||
| primitives: null, | ||
| }); | ||
|
|
||
| const isObjectLike = (value: unknown): value is object => | ||
| (typeof value === 'object' && value !== null) || typeof value === 'function'; | ||
|
|
||
| /** | ||
| * Memoizes a function using a tree of caches keyed by its arguments. | ||
| * Object arguments are held weakly, so cached results can be garbage collected | ||
| * together with the arguments that produced them. | ||
| * | ||
| * Drop-in replacement for the subset of `weakMapMemoize` from `reselect` that MUI X relies on. | ||
| */ | ||
| export function weakMapMemoize<F extends (...args: any[]) => any>(func: F): F { | ||
| const root = createCacheNode(); | ||
|
|
||
| function memoized(...args: any[]) { | ||
| let node = root; | ||
| for (let i = 0; i < args.length; i += 1) { | ||
| const arg = args[i]; | ||
| if (isObjectLike(arg)) { | ||
| if (node.objects === null) { | ||
| node.objects = new WeakMap(); | ||
| } | ||
| let child = node.objects.get(arg); | ||
| if (child === undefined) { | ||
| child = createCacheNode(); | ||
| node.objects.set(arg, child); | ||
| } | ||
| node = child; | ||
| } else { | ||
| if (node.primitives === null) { | ||
| node.primitives = new Map(); | ||
| } | ||
| let child = node.primitives.get(arg); | ||
| if (child === undefined) { | ||
| child = createCacheNode(); | ||
| node.primitives.set(arg, child); | ||
| } | ||
| node = child; | ||
| } | ||
| } | ||
|
|
||
| if (!node.computed) { | ||
| node.value = func(...args); | ||
| node.computed = true; | ||
| } | ||
| return node.value; | ||
| } | ||
|
|
||
| return memoized as unknown as F; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I like about reselect is that it has a fast version of
lruMemoizeoptimized formaxSize: 1, which is our default (and maybe only) value for that option. I guess we'd lose some of the size benefits by doing the same here, I'd be curious to know how much it costs.See https://github.com/reduxjs/reselect/blob/master/src/lruMemoize.ts