From 84ef4a08ceeebee51dd989227a062a05905959a3 Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 15:28:34 +0800 Subject: [PATCH 01/10] =?UTF-8?q?Got=20it,=20let's=20see.=20The=20u?= =?UTF-8?q?ser=20wants=20a=20Git=20commit=20message=20based=20on=20the=20d?= =?UTF-8?q?iffs.=20First,=20I=20need=20to=20understand=20what=20changes=20?= =?UTF-8?q?were=20made.=20\n\nLooking=20at=20the=20diffs:=20\n\n1.=20In=20?= =?UTF-8?q?MeasureCell.tsx,=20they=20added=20an=20import=20for=20sanitizeC?= =?UTF-8?q?loneElement=20and=20used=20it=20on=20the=20title.\n2.=20In=20Me?= =?UTF-8?q?asureRow.tsx,=20they=20removed=20the=20manual=20cloning=20of=20?= =?UTF-8?q?the=20title=20and=20just=20passed=20column=3F.title=20directly,?= =?UTF-8?q?=20relying=20on=20the=20new=20sanitizeCloneElement=20function.\?= =?UTF-8?q?n3.=20They=20added=20a=20new=20file=20reactNodeUtil.tsx=20with?= =?UTF-8?q?=20the=20sanitizeCloneElement=20function,=20which=20clones=20Re?= =?UTF-8?q?act=20elements=20and=20strips=20certain=20props=20(like=20data-?= =?UTF-8?q?*,=20id,=20ref,=20onFocus,=20onBlur)=20to=20avoid=20issues=20in?= =?UTF-8?q?=20table=20cells.\n\nSo=20the=20main=20change=20is=20adding=20a?= =?UTF-8?q?=20utility=20to=20sanitize=20React=20nodes=20for=20table=20cell?= =?UTF-8?q?s,=20which=20is=20a=20refactor=20or=20maybe=20a=20feat=3F=20Wai?= =?UTF-8?q?t,=20the=20type=20should=20be=20based=20on=20the=20change.=20Si?= =?UTF-8?q?nce=20it's=20adding=20a=20new=20utility=20function=20and=20usin?= =?UTF-8?q?g=20it=20to=20clean=20up=20the=20code=20(removing=20manual=20cl?= =?UTF-8?q?oning=20in=20MeasureRow),=20maybe=20it's=20a=20refactor.=20Or?= =?UTF-8?q?=20maybe=20a=20feat=20because=20it's=20a=20new=20feature=3F=20W?= =?UTF-8?q?ait,=20no,=20the=20utility=20is=20to=20fix=20potential=20issues?= =?UTF-8?q?,=20so=20maybe=20refactor.=20Wait,=20let's=20check=20the=20type?= =?UTF-8?q?s:=20fix,=20feat,=20build,=20chore,=20ci,=20docs,=20style,=20re?= =?UTF-8?q?factor,=20perf,=20test.\n\nThe=20changes=20are:=20added=20a=20n?= =?UTF-8?q?ew=20utility=20function=20(reactNodeUtil.tsx)=20and=20updated?= =?UTF-8?q?=20two=20existing=20files=20to=20use=20it.=20So=20this=20is=20a?= =?UTF-8?q?=20refactor=20because=20it's=20restructuring=20the=20code=20to?= =?UTF-8?q?=20use=20a=20new=20utility=20instead=20of=20manual=20cloning.?= =?UTF-8?q?=20Also,=20the=20utility=20is=20meant=20to=20sanitize=20React?= =?UTF-8?q?=20nodes,=20which=20is=20a=20refactor=20to=20improve=20code=20s?= =?UTF-8?q?tructure=20and=20avoid=20duplication.\n\nSo=20the=20commit=20me?= =?UTF-8?q?ssage=20should=20be=20something=20like=20"refactor:=20add=20san?= =?UTF-8?q?itizeCloneElement=20utility=20to=20clean=20React=20nodes=20for?= =?UTF-8?q?=20table=20cells".=20Wait,=20but=20need=20to=20be=20concise.=20?= =?UTF-8?q?Let's=20check=20the=20length.=20Let's=20see:=20"refactor:=20add?= =?UTF-8?q?=20sanitizeCloneElement=20utility=20to=20sanitize=20React=20nod?= =?UTF-8?q?es=20in=20table=20cells".=20That's=20under=2072=20characters.?= =?UTF-8?q?=20Wait,=20let's=20count:=20"refactor:=20add=20sanitizeCloneEle?= =?UTF-8?q?ment=20utility=20to=20sanitize=20React=20nodes=20in=20table=20c?= =?UTF-8?q?ells"=20=E2=80=93=20let's=20see:=20"refactor:=20add=20sanitizeC?= =?UTF-8?q?loneElement=20utility=20to=20sanitize=20React=20nodes=20in=20ta?= =?UTF-8?q?ble=20cells"=20=E2=80=93=20that's=2068=20characters.=20Perfect.?= =?UTF-8?q?\n\nWait,=20but=20let's=20make=20sure=20the=20type=20is=20corre?= =?UTF-8?q?ct.=20The=20change=20is=20adding=20a=20new=20utility=20function?= =?UTF-8?q?=20and=20using=20it=20to=20replace=20manual=20cloning=20in=20tw?= =?UTF-8?q?o=20places.=20So=20it's=20a=20refactor=20because=20it's=20restr?= =?UTF-8?q?ucturing=20the=20code=20to=20use=20a=20new=20utility=20instead?= =?UTF-8?q?=20of=20duplicating=20logic.=20Yes,=20that's=20a=20refactor.\n\?= =?UTF-8?q?nSo=20the=20commit=20message=20should=20be=20"refactor:=20add?= =?UTF-8?q?=20sanitizeCloneElement=20utility=20to=20sanitize=20React=20nod?= =?UTF-8?q?es=20in=20table=20cells".\n<|begin=5Fof=5Fbox|>refactor?= =?UTF-8?q?:=20add=20sanitizeCloneElement=20utility=20to=20sanitize=20Reac?= =?UTF-8?q?t=20nodes=20in=20table=20cells<|end=5Fof=5Fbox|>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Body/MeasureCell.tsx | 5 ++++- src/Body/MeasureRow.tsx | 6 +----- src/utils/reactNodeUtil.tsx | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 src/utils/reactNodeUtil.tsx diff --git a/src/Body/MeasureCell.tsx b/src/Body/MeasureCell.tsx index 11b861148..e747b0148 100644 --- a/src/Body/MeasureCell.tsx +++ b/src/Body/MeasureCell.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import ResizeObserver from 'rc-resize-observer'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; +import { sanitizeCloneElement } from '../utils/reactNodeUtil'; export interface MeasureCellProps { columnKey: React.Key; @@ -26,7 +27,9 @@ export default function MeasureCell({ return ( -
{title || '\xa0'}
+
+ {sanitizeCloneElement(title) || '\xa0'} +
); diff --git a/src/Body/MeasureRow.tsx b/src/Body/MeasureRow.tsx index c0fda2e52..3ca9c6b6f 100644 --- a/src/Body/MeasureRow.tsx +++ b/src/Body/MeasureRow.tsx @@ -35,17 +35,13 @@ export default function MeasureRow({ > {columnsKey.map(columnKey => { const column = columns.find(col => col.key === columnKey); - const rawTitle = column?.title; - const titleForMeasure = React.isValidElement>(rawTitle) - ? React.cloneElement(rawTitle, { ref: null }) - : rawTitle; return ( ); })} diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx new file mode 100644 index 000000000..0bafeef21 --- /dev/null +++ b/src/utils/reactNodeUtil.tsx @@ -0,0 +1,37 @@ +import React, { cloneElement, isValidElement, memo } from 'react'; + +const stripProps = (props: Record) => { + const result: Record = {}; + for (const key in props) { + // strip data-*、id、ref, onFocus、onBlur + if ( + key === 'id' || + key === 'ref' || + key === 'onFocus' || + key === 'onBlur' || + key.startsWith('data-') + ) + continue; + result[key] = props[key]; + } + return result; +}; + +/** + * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props + * to avoid potential issues with nested elements in table cells. + */ +export const sanitizeCloneElement = memo(function sanitizeCloneElement( + node: React.ReactNode, +): React.ReactNode { + if (!isValidElement(node)) { + return node; + } + const cleanedProps = stripProps(node.props); + if (cleanedProps.children) { + cleanedProps.children = React.Children.map(cleanedProps.children, child => + sanitizeCloneElement(child), + ); + } + return cloneElement(node, cleanedProps); +}); From d887109db0bbc603341d0df18680bc7a4518140f Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:01:25 +0800 Subject: [PATCH 02/10] fix --- src/utils/reactNodeUtil.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index 0bafeef21..3fba14e55 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -21,7 +21,7 @@ const stripProps = (props: Record) => { * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props * to avoid potential issues with nested elements in table cells. */ -export const sanitizeCloneElement = memo(function sanitizeCloneElement( +export const sanitizeCloneElement = memo(function sanitizeClone( node: React.ReactNode, ): React.ReactNode { if (!isValidElement(node)) { @@ -30,7 +30,7 @@ export const sanitizeCloneElement = memo(function sanitizeCloneElement( const cleanedProps = stripProps(node.props); if (cleanedProps.children) { cleanedProps.children = React.Children.map(cleanedProps.children, child => - sanitizeCloneElement(child), + sanitizeClone(child), ); } return cloneElement(node, cleanedProps); From 46291a5f15f94bf5dfadfa4ec6e6ba8965b31418 Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:02:47 +0800 Subject: [PATCH 03/10] bump rc-table --- src/utils/reactNodeUtil.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index 3fba14e55..b1337fe77 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -9,6 +9,7 @@ const stripProps = (props: Record) => { key === 'ref' || key === 'onFocus' || key === 'onBlur' || + key === 'tabIndex' || key.startsWith('data-') ) continue; From e7dd3c3bbae6f6a746e4d68ade43227053053ea3 Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:10:02 +0800 Subject: [PATCH 04/10] fix --- src/utils/reactNodeUtil.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index b1337fe77..11f3be3b1 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -1,4 +1,4 @@ -import React, { cloneElement, isValidElement, memo } from 'react'; +import React, { cloneElement, isValidElement } from 'react'; const stripProps = (props: Record) => { const result: Record = {}; @@ -22,17 +22,17 @@ const stripProps = (props: Record) => { * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props * to avoid potential issues with nested elements in table cells. */ -export const sanitizeCloneElement = memo(function sanitizeClone( - node: React.ReactNode, -): React.ReactNode { +const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { if (!isValidElement(node)) { return node; } const cleanedProps = stripProps(node.props); if (cleanedProps.children) { cleanedProps.children = React.Children.map(cleanedProps.children, child => - sanitizeClone(child), + sanitizeCloneElement(child), ); } return cloneElement(node, cleanedProps); -}); +}; + +export { sanitizeCloneElement }; From 4c87fa7668f5767e53550ed1e311c81a6ead22dd Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:15:03 +0800 Subject: [PATCH 05/10] add memo --- src/utils/reactNodeUtil.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index 11f3be3b1..d54f6f410 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -1,4 +1,4 @@ -import React, { cloneElement, isValidElement } from 'react'; +import React, { cloneElement, isValidElement, memo } from 'react'; const stripProps = (props: Record) => { const result: Record = {}; @@ -22,7 +22,7 @@ const stripProps = (props: Record) => { * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props * to avoid potential issues with nested elements in table cells. */ -const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { +const sanitizeCloneElement = memo((node: React.ReactNode): React.ReactNode => { if (!isValidElement(node)) { return node; } @@ -33,6 +33,6 @@ const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { ); } return cloneElement(node, cleanedProps); -}; +}); export { sanitizeCloneElement }; From e967277e0d89d09e7f902a2db725e0c7f46be36a Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:17:13 +0800 Subject: [PATCH 06/10] remove memo --- src/utils/reactNodeUtil.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index d54f6f410..11f3be3b1 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -1,4 +1,4 @@ -import React, { cloneElement, isValidElement, memo } from 'react'; +import React, { cloneElement, isValidElement } from 'react'; const stripProps = (props: Record) => { const result: Record = {}; @@ -22,7 +22,7 @@ const stripProps = (props: Record) => { * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props * to avoid potential issues with nested elements in table cells. */ -const sanitizeCloneElement = memo((node: React.ReactNode): React.ReactNode => { +const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { if (!isValidElement(node)) { return node; } @@ -33,6 +33,6 @@ const sanitizeCloneElement = memo((node: React.ReactNode): React.ReactNode => { ); } return cloneElement(node, cleanedProps); -}); +}; export { sanitizeCloneElement }; From 247a1da29682ecc89851140a4b34d15575d6c650 Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:22:20 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/reactNodeUtil.tsx | 78 ++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index 11f3be3b1..642e48a35 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -1,38 +1,86 @@ import React, { cloneElement, isValidElement } from 'react'; -const stripProps = (props: Record) => { - const result: Record = {}; +// 预定义需要剥离的属性集合,提高查找性能 +const STRIP_PROPS = new Set(['id', 'ref', 'onFocus', 'onBlur', 'tabIndex']); + +// 使用 WeakMap 缓存已处理的 props,避免重复计算 +const propsCache = new WeakMap, Record>(); + +const stripProps = (props: Record) => { + // 检查缓存 + const cached = propsCache.get(props); + if (cached) { + return cached; + } + + let hasChanges = false; + const result: Record = {}; + for (const key in props) { - // strip data-*、id、ref, onFocus、onBlur - if ( - key === 'id' || - key === 'ref' || - key === 'onFocus' || - key === 'onBlur' || - key === 'tabIndex' || - key.startsWith('data-') - ) + // 使用 Set 进行 O(1) 查找,优化 data-* 属性检查 + if (STRIP_PROPS.has(key) || key.startsWith('data-')) { + hasChanges = true; continue; + } result[key] = props[key]; } - return result; + + // 如果没有需要剥离的属性,直接返回原对象 + const finalResult = hasChanges ? result : props; + + // 缓存结果 + propsCache.set(props, finalResult); + + return finalResult; }; +// 使用 WeakMap 缓存已处理的节点,避免重复处理 +const nodeCache = new WeakMap(); + /** * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props * to avoid potential issues with nested elements in table cells. + * + * 优化特性: + * 1. 缓存机制避免重复处理相同节点 + * 2. 提前退出条件减少不必要的递归 + * 3. 浅层优化:如果props没有变化,直接返回原节点 */ const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { if (!isValidElement(node)) { return node; } - const cleanedProps = stripProps(node.props); + + // 检查缓存 + const cached = nodeCache.get(node); + if (cached) { + return cached; + } + + const cleanedProps = stripProps(node.props as Record); + + // 如果props没有变化且没有children需要处理,直接返回原节点 + if (cleanedProps === node.props && !cleanedProps.children) { + nodeCache.set(node, node); + return node; + } + + let processedChildren = cleanedProps.children; if (cleanedProps.children) { - cleanedProps.children = React.Children.map(cleanedProps.children, child => + processedChildren = React.Children.map(cleanedProps.children as React.ReactNode, child => sanitizeCloneElement(child), ); } - return cloneElement(node, cleanedProps); + + const result = cloneElement(node, { + ...cleanedProps, + children: processedChildren, + } as React.Attributes); + + // 缓存结果 + nodeCache.set(node, result); + + return result; }; export { sanitizeCloneElement }; From a3bdc1ef000ff4119533685a257a91ac3e9428a8 Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:26:19 +0800 Subject: [PATCH 08/10] fix: measurerow sanitizeCloneElement --- src/utils/reactNodeUtil.tsx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index 642e48a35..58a1d868d 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -1,13 +1,13 @@ import React, { cloneElement, isValidElement } from 'react'; -// 预定义需要剥离的属性集合,提高查找性能 +// Predefined set of props to strip for better lookup performance const STRIP_PROPS = new Set(['id', 'ref', 'onFocus', 'onBlur', 'tabIndex']); -// 使用 WeakMap 缓存已处理的 props,避免重复计算 +// Use WeakMap to cache processed props and avoid duplicate calculations const propsCache = new WeakMap, Record>(); const stripProps = (props: Record) => { - // 检查缓存 + // Check cache first const cached = propsCache.get(props); if (cached) { return cached; @@ -17,7 +17,7 @@ const stripProps = (props: Record) => { const result: Record = {}; for (const key in props) { - // 使用 Set 进行 O(1) 查找,优化 data-* 属性检查 + // Use Set for O(1) lookup, optimize data-* attribute checking if (STRIP_PROPS.has(key) || key.startsWith('data-')) { hasChanges = true; continue; @@ -25,33 +25,33 @@ const stripProps = (props: Record) => { result[key] = props[key]; } - // 如果没有需要剥离的属性,直接返回原对象 + // If no props need to be stripped, return original object directly const finalResult = hasChanges ? result : props; - // 缓存结果 + // Cache the result propsCache.set(props, finalResult); return finalResult; }; -// 使用 WeakMap 缓存已处理的节点,避免重复处理 +// Use WeakMap to cache processed nodes and avoid duplicate processing const nodeCache = new WeakMap(); /** * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props * to avoid potential issues with nested elements in table cells. * - * 优化特性: - * 1. 缓存机制避免重复处理相同节点 - * 2. 提前退出条件减少不必要的递归 - * 3. 浅层优化:如果props没有变化,直接返回原节点 + * Optimization features: + * 1. Caching mechanism to avoid reprocessing the same nodes + * 2. Early exit conditions to reduce unnecessary recursion + * 3. Shallow optimization: return original node directly if props haven't changed */ const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { if (!isValidElement(node)) { return node; } - // 检查缓存 + // Check cache first const cached = nodeCache.get(node); if (cached) { return cached; @@ -59,7 +59,7 @@ const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { const cleanedProps = stripProps(node.props as Record); - // 如果props没有变化且没有children需要处理,直接返回原节点 + // If props haven't changed and no children need processing, return original node directly if (cleanedProps === node.props && !cleanedProps.children) { nodeCache.set(node, node); return node; @@ -77,7 +77,7 @@ const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { children: processedChildren, } as React.Attributes); - // 缓存结果 + // Cache the result nodeCache.set(node, result); return result; From c5257ea5a792fdf8bda77f4af0e206217717c7ce Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:32:06 +0800 Subject: [PATCH 09/10] fix --- src/utils/reactNodeUtil.tsx | 92 +++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index 58a1d868d..61c11aa34 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -1,86 +1,76 @@ import React, { cloneElement, isValidElement } from 'react'; -// Predefined set of props to strip for better lookup performance -const STRIP_PROPS = new Set(['id', 'ref', 'onFocus', 'onBlur', 'tabIndex']); +// Props 类型定义 +type Props = Record; -// Use WeakMap to cache processed props and avoid duplicate calculations -const propsCache = new WeakMap, Record>(); +// 缓存已处理的 props,避免重复计算 +const propsCache = new WeakMap(); -const stripProps = (props: Record) => { - // Check cache first - const cached = propsCache.get(props); - if (cached) { - return cached; +// 需要过滤的属性集合,使用 Set 提高查找性能 +const FILTERED_PROPS = new Set(['id', 'ref', 'onFocus', 'onBlur', 'tabIndex']); + +const stripProps = (props: Props) => { + // 检查缓存 + if (propsCache.has(props)) { + const cachedProps = propsCache.get(props); + return cachedProps || props; } - let hasChanges = false; - const result: Record = {}; + const result: Props = {}; + let hasFilteredProps = false; for (const key in props) { - // Use Set for O(1) lookup, optimize data-* attribute checking - if (STRIP_PROPS.has(key) || key.startsWith('data-')) { - hasChanges = true; + // 使用 Set 快速查找,避免多个 || 判断 + if (FILTERED_PROPS.has(key) || key.startsWith('data-')) { + hasFilteredProps = true; continue; } result[key] = props[key]; } - // If no props need to be stripped, return original object directly - const finalResult = hasChanges ? result : props; - - // Cache the result - propsCache.set(props, finalResult); + // 如果没有需要过滤的属性,直接返回原 props + if (!hasFilteredProps) { + propsCache.set(props, props); + return props; + } - return finalResult; + // 缓存结果 + propsCache.set(props, result); + return result; }; -// Use WeakMap to cache processed nodes and avoid duplicate processing -const nodeCache = new WeakMap(); - /** * Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props * to avoid potential issues with nested elements in table cells. - * - * Optimization features: - * 1. Caching mechanism to avoid reprocessing the same nodes - * 2. Early exit conditions to reduce unnecessary recursion - * 3. Shallow optimization: return original node directly if props haven't changed + * @param node - React node to sanitize + * @param depth - Current recursion depth (for performance control) + * @param maxDepth - Maximum recursion depth to prevent stack overflow */ -const sanitizeCloneElement = (node: React.ReactNode): React.ReactNode => { - if (!isValidElement(node)) { +const sanitizeCloneElement = ( + node: React.ReactNode, + depth: number = 0, + maxDepth: number = 10, +): React.ReactNode => { + // 限制递归深度,防止性能问题和堆栈溢出 + if (depth >= maxDepth || !isValidElement(node)) { return node; } - // Check cache first - const cached = nodeCache.get(node); - if (cached) { - return cached; - } - - const cleanedProps = stripProps(node.props as Record); + const cleanedProps = stripProps(node.props); - // If props haven't changed and no children need processing, return original node directly + // 如果 props 没有变化且没有 children,直接返回原节点 if (cleanedProps === node.props && !cleanedProps.children) { - nodeCache.set(node, node); return node; } - let processedChildren = cleanedProps.children; + // 处理 children if (cleanedProps.children) { - processedChildren = React.Children.map(cleanedProps.children as React.ReactNode, child => - sanitizeCloneElement(child), + cleanedProps.children = React.Children.map(cleanedProps.children, (child: React.ReactNode) => + sanitizeCloneElement(child, depth + 1, maxDepth), ); } - const result = cloneElement(node, { - ...cleanedProps, - children: processedChildren, - } as React.Attributes); - - // Cache the result - nodeCache.set(node, result); - - return result; + return cloneElement(node, cleanedProps); }; export { sanitizeCloneElement }; From 47086bd9f2672f37b02508763b6d56c8cca5a8cd Mon Sep 17 00:00:00 2001 From: afc163 Date: Thu, 9 Oct 2025 16:33:55 +0800 Subject: [PATCH 10/10] test: add comprehensive tests for sanitizeCloneElement utility function --- src/utils/__tests__/reactNodeUtil.test.tsx | 106 +++++++++++++++++++++ src/utils/reactNodeUtil.tsx | 20 ++-- 2 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 src/utils/__tests__/reactNodeUtil.test.tsx diff --git a/src/utils/__tests__/reactNodeUtil.test.tsx b/src/utils/__tests__/reactNodeUtil.test.tsx new file mode 100644 index 000000000..9ce62ee45 --- /dev/null +++ b/src/utils/__tests__/reactNodeUtil.test.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { sanitizeCloneElement } from '../reactNodeUtil'; + +describe('reactNodeUtil', () => { + describe('sanitizeCloneElement', () => { + it('should remove filtered props', () => { + const node = React.createElement( + 'div', + { + id: 'test-id', + 'data-test': 'test-data', + onFocus: () => {}, + onBlur: () => {}, + tabIndex: 0, + className: 'test-class', + }, + 'test content', + ); + + const result = sanitizeCloneElement(node) as React.ReactElement; + + expect(result.props.id).toBeUndefined(); + expect(result.props['data-test']).toBeUndefined(); + expect(result.props.onFocus).toBeUndefined(); + expect(result.props.onBlur).toBeUndefined(); + expect(result.props.tabIndex).toBeUndefined(); + expect(result.props.className).toBe('test-class'); + expect(result.props.children).toBe('test content'); + }); + + it('should handle nested elements', () => { + const nested = React.createElement('span', { id: 'nested-id' }, 'nested'); + const parent = React.createElement( + 'div', + { + id: 'parent-id', + className: 'parent', + }, + nested, + ); + + const result = sanitizeCloneElement(parent) as React.ReactElement; + + expect(result.props.id).toBeUndefined(); + expect(result.props.className).toBe('parent'); + + const childElement = result.props.children as React.ReactElement; + expect(childElement.props.id).toBeUndefined(); + expect(childElement.props.children).toBe('nested'); + }); + + it('should limit recursion depth', () => { + // 创建深层嵌套结构 + let deepNode: React.ReactElement = React.createElement('span', { id: 'deep' }, 'content'); + + for (let i = 0; i < 15; i++) { + deepNode = React.createElement('div', { id: `level-${i}` }, deepNode); + } + + const result = sanitizeCloneElement(deepNode, 0, 5); + expect(result).toBeDefined(); + // 应该在递归深度限制内正常工作 + }); + + it('should return non-React elements unchanged', () => { + const textNode = 'plain text'; + const numberNode = 42; + const nullNode = null; + + expect(sanitizeCloneElement(textNode)).toBe(textNode); + expect(sanitizeCloneElement(numberNode)).toBe(numberNode); + expect(sanitizeCloneElement(nullNode)).toBe(nullNode); + }); + + it('should use cache for performance', () => { + const props = { className: 'test', id: 'will-be-removed' }; + const node1 = React.createElement('div', props, 'content'); + const node2 = React.createElement('span', props, 'other content'); + + const result1 = sanitizeCloneElement(node1) as React.ReactElement; + const result2 = sanitizeCloneElement(node2) as React.ReactElement; + + // 虽然是不同的元素,但相同的 props 应该被缓存 + expect(result1.props.className).toBe('test'); + expect(result1.props.id).toBeUndefined(); + expect(result2.props.className).toBe('test'); + expect(result2.props.id).toBeUndefined(); + }); + + it('should return original node if no props need filtering', () => { + const node = React.createElement( + 'div', + { + className: 'test', + title: 'clean', + }, + 'content', + ); + + const result = sanitizeCloneElement(node); + + // 如果没有需要过滤的属性,应该返回原节点以提高性能 + expect(result).toBe(node); + }); + }); +}); diff --git a/src/utils/reactNodeUtil.tsx b/src/utils/reactNodeUtil.tsx index 61c11aa34..6f9e898b3 100644 --- a/src/utils/reactNodeUtil.tsx +++ b/src/utils/reactNodeUtil.tsx @@ -1,16 +1,16 @@ import React, { cloneElement, isValidElement } from 'react'; -// Props 类型定义 +// Props type definition type Props = Record; -// 缓存已处理的 props,避免重复计算 +// Cache processed props to avoid redundant calculations const propsCache = new WeakMap(); -// 需要过滤的属性集合,使用 Set 提高查找性能 +// Set of properties to filter, using Set for better lookup performance const FILTERED_PROPS = new Set(['id', 'ref', 'onFocus', 'onBlur', 'tabIndex']); const stripProps = (props: Props) => { - // 检查缓存 + // Check cache first if (propsCache.has(props)) { const cachedProps = propsCache.get(props); return cachedProps || props; @@ -20,7 +20,7 @@ const stripProps = (props: Props) => { let hasFilteredProps = false; for (const key in props) { - // 使用 Set 快速查找,避免多个 || 判断 + // Use Set for fast lookup, avoiding multiple || conditions if (FILTERED_PROPS.has(key) || key.startsWith('data-')) { hasFilteredProps = true; continue; @@ -28,13 +28,13 @@ const stripProps = (props: Props) => { result[key] = props[key]; } - // 如果没有需要过滤的属性,直接返回原 props + // If no props need filtering, return original props directly if (!hasFilteredProps) { propsCache.set(props, props); return props; } - // 缓存结果 + // Cache the result propsCache.set(props, result); return result; }; @@ -51,19 +51,19 @@ const sanitizeCloneElement = ( depth: number = 0, maxDepth: number = 10, ): React.ReactNode => { - // 限制递归深度,防止性能问题和堆栈溢出 + // Limit recursion depth to prevent performance issues and stack overflow if (depth >= maxDepth || !isValidElement(node)) { return node; } const cleanedProps = stripProps(node.props); - // 如果 props 没有变化且没有 children,直接返回原节点 + // If props haven't changed and no children, return original node directly if (cleanedProps === node.props && !cleanedProps.children) { return node; } - // 处理 children + // Process children if (cleanedProps.children) { cleanedProps.children = React.Children.map(cleanedProps.children, (child: React.ReactNode) => sanitizeCloneElement(child, depth + 1, maxDepth),