在反应中,我确实经常需要使用诸如 useCallback
之类的内容来记忆项目列表中的函数(通过循环创建),以避免由于引用标识符不匹配而导致单个元素发生更改而重新渲染所有组件……不幸的是,这是令人惊讶的是很难到期。例如,考虑以下代码:
const MyComp = memo({elements} => { { elements.map((elt, i) => { <>{elt.text}<Button onClick={(e) => dispatch(removeElement({id: i}))}> <> }) } })
其中 Button
是由 ant design 等提供的外部组件。然后,这个函数引用在每次渲染时都会不同,因为它是内联的,因此强制重新渲染。
为了避免这个问题,我可以想到另一种解决方案:创建一个新组件 MyButton
,它接受两个属性 index={i}
和 onClick
而不是单个 onClick
,并将参数 index
附加到任何调用onClick
:
const MyButton = ({myOnClick, id, ...props}) => { const myOnClickUnwrap = useCallback(e => myOnClick(e, id), [myOnClick]); return <Button onClick={myOnClickUnwrap} ...props/> }; const MyComp = memo({elements} => { const myOnClick = useCallback((e, id) => dispatch(removeElement({id: id})), []); return { elements.map((elt, i) => { <>{elt.text}<Button id={i} onClick={myOnClick}> <> }) } )
虽然这确实有效,但由于多种原因,这是非常不实用的:
Button
等外部库中的所有元素,并重写原本不打算处理这种嵌套的组件……这会破坏模块化并使代码更加复杂<MyButton index1={index1} index2={index2} index3={index3 onClick={myFunction}>
,这意味着我需要完全通用地创建一个更复杂的版本 MyButton
来检查嵌套级别的数量。我无法使用 index={[index1,index2,index3]}
,因为这是一个数组,因此没有稳定的引用。index
es 的命名没有约定,这意味着项目之间共享代码或开发库更加困难我缺少更好的解决方案吗?考虑到列表无处不在,我不敢相信对此没有适当的解决方案,而且我很惊讶地看到这方面的文档很少。
编辑 我尝试这样做:
// Define once: export const WrapperOnChange = memo(({onChange, index, Component, ...props}) => { const onChangeWrapped = useCallback(e => onChange(e, index), [onChange, index]); return <Component {...props} onChange={onChangeWrapped} /> }); export const WrapperOnClick = memo(({onClick, index, Component, ...props}) => { const onClickWrapped = useCallback(e => onClick(e, index), [onClick, index]); return <Component {...props} onClick={onClickWrapped} /> });
并像这样使用它:
const myactionIndexed = useCallback((e, i) => dispatch(removeSolverConstraint({id: i})), []); return <WrapperOnClick index={i} Component={Button} onClick={myactionIndexed} danger><CloseCircleOutlined /></WrapperOnClick>但这仍然不完美,特别是我需要一个用于不同嵌套级别的包装器,每当我定位一个新属性时我都需要创建一个新版本(
onClick
,onChange
,...),如果我有它就无法直接工作多个属性(例如 onClick
和 onChange
),我以前从未见过这个,所以我想有更好的解决方案。
编辑 我尝试了各种想法,包括使用 fast-memoize,但我仍然不明白所有结果:有时,fast-memoize 有效,有时失败......而且我不知道 fast-memoize 是否是推荐的解决方案:似乎对于如此常见的用例使用第三方工具很奇怪。在这里查看我的测试https://codesandbox.io/embed/magical-dawn-67mgxp?fontsize=14&hidenavigation=1&theme=dark
这里的测试 https://codesandbox.io /s/sharp-wind-rd48q4?file=/src/App.js
警告:我不是 React 专家(因此我的问题!),所以请在下面发表评论和/或添加 +1,如果您认为此解决方案是在 React 中进行的规范方法(或 -1不是^^)。我也很好奇为什么其他一些解决方案失败了(例如基于 proxy-memoize(实际上比没有缓存长 10 倍,并且根本不缓存)或 fast-memoize(并不总是缓存,具体取决于如何我使用它)),所以如果你知道我有兴趣知道)
由于我对这个问题没什么兴趣,所以我尝试根据各种选择(无记忆、使用外部库(快速记忆与代理记忆)、使用包装器)对一堆解决方案(14!)进行基准测试,使用外部组件等...
最好的方法似乎是创建一个新组件包含列表的整个元素,而不仅仅是最后一个按钮。这允许相当干净的代码(即使我需要为列表和项目创建两个组件,至少它在语义上是有意义的),避免外部库,并且似乎比我尝试过的所有其他方法更有效(至少以我的例子为例):
我仍然不太喜欢这个解决方案,因为我需要将许多内容从父组件转发到子组件,但这似乎是我能得到的最佳解决方案......
您可以查看我的尝试列表 这里,我使用了下面的代码。这是探查器的视图(从技术上讲,所有版本之间的时间差异并不大(除了使用 proxy-memoize 的版本 7,我删除了它,因为它更长,也许是 10 倍,并且正在制作图表)更难阅读),但我预计这种差异在较长的列表上会更大,其中项目绘制起来更加复杂(这里我只有一个文本和一个按钮)。请注意,所有版本并不完全相同(有些版本使用
,一些
,一些普通列表,一些 Ant 设计列表...),所以时间比较只有在执行相同操作的版本之间才有意义。无论如何,我主要关心查看缓存的内容和未缓存的内容,这在分析器中清晰可见(缓存了浅灰色块):
另一个有趣的事实是,您可能希望在记忆之前进行基准测试,因为改进可能并不显着,至少对于简单的组件而言(此处大小为 5,只有一个文本和一个按钮)。