Skip to content

feat(core): Optimize dropUndefinedKeys #15760

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

Merged
merged 5 commits into from
Mar 21, 2025
Merged
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
71 changes: 34 additions & 37 deletions packages/core/src/utils-hoist/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { WrappedFunction } from '../types-hoist';

import { htmlTreeAsString } from './browser';
import { DEBUG_BUILD } from './debug-build';
import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive } from './is';
import { isElement, isError, isEvent, isInstanceOf, isPrimitive } from './is';
import { logger } from './logger';
import { truncate } from './string';

Expand Down Expand Up @@ -222,58 +222,55 @@ export function dropUndefinedKeys<T>(inputValue: T): T {
}

function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unknown>): T {
if (isPojo(inputValue)) {
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
const memoVal = memoizationMap.get(inputValue);
if (memoVal !== undefined) {
return memoVal as T;
}

const returnValue: { [key: string]: unknown } = {};
// Store the mapping of this value in case we visit it again, in case of circular data
memoizationMap.set(inputValue, returnValue);

for (const key of Object.getOwnPropertyNames(inputValue)) {
if (typeof inputValue[key] !== 'undefined') {
returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap);
}
}
// Early return for primitive values
if (inputValue === null || typeof inputValue !== 'object') {
return inputValue;
}

return returnValue as T;
// Check memo map first for all object types
const memoVal = memoizationMap.get(inputValue);
if (memoVal !== undefined) {
return memoVal as T;
}

// handle arrays
if (Array.isArray(inputValue)) {
// If this node has already been visited due to a circular reference, return the array it was mapped to in the new object
const memoVal = memoizationMap.get(inputValue);
if (memoVal !== undefined) {
return memoVal as T;
}

const returnValue: unknown[] = [];
// Store the mapping of this value in case we visit it again, in case of circular data
// Store mapping to handle circular references
memoizationMap.set(inputValue, returnValue);

inputValue.forEach((item: unknown) => {
returnValue.push(_dropUndefinedKeys(item, memoizationMap));
inputValue.forEach(value => {
returnValue.push(_dropUndefinedKeys(value, memoizationMap));
});

return returnValue as unknown as T;
}

if (isPojo(inputValue)) {
const returnValue: { [key: string]: unknown } = {};
// Store mapping to handle circular references
memoizationMap.set(inputValue, returnValue);

const keys = Object.keys(inputValue);

keys.forEach(key => {
const val = inputValue[key];
if (val !== undefined) {
returnValue[key] = _dropUndefinedKeys(val, memoizationMap);
}
});

return returnValue as T;
}

// For other object types, return as is
return inputValue;
}

function isPojo(input: unknown): input is Record<string, unknown> {
if (!isPlainObject(input)) {
return false;
}

try {
const name = (Object.getPrototypeOf(input) as { constructor: { name: string } }).constructor.name;
return !name || name === 'Object';
} catch {
return true;
}
// Plain objects have Object as constructor or no constructor
const constructor = (input as object).constructor;
return constructor === Object || constructor === undefined;
}

/**
Expand Down
Loading