-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
refactor(query-core): improve replaceEqualDeep performance #9604
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
Changes from 6 commits
e6029db
5e527f3
1007495
25b5bbd
3c69c31
d225d24
91494ca
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 |
---|---|---|
|
@@ -245,50 +245,104 @@ export function partialMatchKey(a: any, b: any): boolean { | |
return false | ||
} | ||
|
||
const hasOwn = Object.prototype.hasOwnProperty | ||
|
||
/** | ||
* This function returns `a` if `b` is deeply equal. | ||
* If not, it will replace any deeply equal children of `b` with those of `a`. | ||
* This can be used for structural sharing between JSON values for example. | ||
*/ | ||
export function replaceEqualDeep<T>(a: unknown, b: T): T | ||
export function replaceEqualDeep(a: any, b: any): any { | ||
export function replaceEqualDeep(a: unknown, b: unknown): any { | ||
if (a === b) { | ||
return a | ||
} | ||
|
||
const array = isPlainArray(a) && isPlainArray(b) | ||
|
||
if (array || (isPlainObject(a) && isPlainObject(b))) { | ||
const aItems = array ? a : Object.keys(a) | ||
const aSize = aItems.length | ||
const bItems = array ? b : Object.keys(b) | ||
const bSize = bItems.length | ||
const copy: any = array ? [] : {} | ||
const aItemsSet = new Set(aItems) | ||
const aIsArr = isPlainArray(a) | ||
const bIsArr = isPlainArray(b) | ||
|
||
// both are arrays | ||
if (aIsArr && bIsArr) { | ||
const aSize = a.length | ||
const bSize = b.length | ||
const copy: Array<unknown> = new Array(bSize) | ||
let equalItems = 0 | ||
|
||
for (let i = 0; i < bSize; i++) { | ||
const key = array ? i : bItems[i] | ||
const aItem = a[i] | ||
const bItem = b[i] | ||
|
||
// most common case (strict equality) | ||
if (aItem === bItem) { | ||
copy[i] = aItem | ||
if (i < aSize) equalItems++ | ||
continue | ||
} | ||
|
||
// either item is not an array or object | ||
if ( | ||
((!array && aItemsSet.has(key)) || array) && | ||
a[key] === undefined && | ||
b[key] === undefined | ||
aItem === null || | ||
bItem === null || | ||
typeof aItem !== 'object' || | ||
typeof bItem !== 'object' | ||
) { | ||
copy[key] = undefined | ||
equalItems++ | ||
} else { | ||
copy[key] = replaceEqualDeep(a[key], b[key]) | ||
if (copy[key] === a[key] && a[key] !== undefined) { | ||
equalItems++ | ||
} | ||
copy[i] = bItem | ||
continue | ||
} | ||
|
||
const v = replaceEqualDeep(aItem, bItem) | ||
copy[i] = v | ||
if (v === aItem) equalItems++ | ||
} | ||
|
||
return aSize === bSize && equalItems === aSize ? a : copy | ||
} | ||
|
||
return b | ||
// only 1 is an array | ||
if (aIsArr || bIsArr) { | ||
return b | ||
} | ||
|
||
// at least 1 is not an object | ||
if (!isPlainObject(a) || !isPlainObject(b)) { | ||
return b | ||
} | ||
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. could the "only 1 is an array" case not be removed because we would also go into the "at least 1 is not an object" case and also all tests still pass if I just remove that check, and we could then also inline 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. yeah it could, it's just less expensive since we already have the booleans, and |
||
|
||
const aSize = Object.keys(a).length | ||
const copy: Record<PropertyKey, unknown> = {} | ||
let equalItems = 0 | ||
let bSize = 0 | ||
|
||
for (const k in b) { | ||
bSize++ | ||
|
||
const aItem = a[k] | ||
const bItem = b[k] | ||
|
||
// most common case (strict equality) | ||
if (aItem === bItem) { | ||
copy[k] = aItem | ||
if (hasOwn.call(a, k)) equalItems++ | ||
continue | ||
} | ||
|
||
// either item is not an array or object | ||
if ( | ||
aItem === null || | ||
bItem === null || | ||
typeof aItem !== 'object' || | ||
typeof bItem !== 'object' | ||
) { | ||
copy[k] = bItem | ||
continue | ||
} | ||
|
||
const v = replaceEqualDeep(aItem, bItem) | ||
copy[k] = v | ||
if (v === aItem) equalItems++ | ||
} | ||
|
||
return aSize === bSize && equalItems === aSize ? a : copy | ||
} | ||
|
||
/** | ||
|
@@ -311,13 +365,12 @@ export function shallowEqualObjects<T extends Record<string, any>>( | |
return true | ||
} | ||
|
||
export function isPlainArray(value: unknown) { | ||
export function isPlainArray(value: unknown): value is Array<unknown> { | ||
return Array.isArray(value) && value.length === Object.keys(value).length | ||
} | ||
|
||
// Copied from: https://github.com/jonschlinkert/is-plain-object | ||
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types | ||
export function isPlainObject(o: any): o is Object { | ||
export function isPlainObject(o: any): o is Record<PropertyKey, unknown> { | ||
if (!hasObjectPrototype(o)) { | ||
return false | ||
} | ||
|