前言
由于之前Vue
开发的习惯,所有函数都定义成了const
函数,而在useEffect
中,如果不再依赖中引入这些函数eslint
就会报warning
,但遇到了一些问题,useEffect
在不期望的时候执行多遍
问题
有如下代码:
依赖中既有redux
中数据,又有常量函数updateGraph
,测试发现额外的点击,会触发这段代码的多次执行,比如我切换侧边栏,引发了侧边栏的重新渲染,这段代码就会不必要的执行一次,令人费解
useEffect(() => {
messageApi.info('waitEdgeNodes.length:' + waitEdgeNodes.length);
//...
}, [waitEdgeNodes.length, updateGraph]);
原因
在组件的主入口有如下代码,切换侧边栏时,由于顶层组件拥有状态option
,所以它的改变会导致子组件的重新渲染,而发生问题的组件就是子组件<SysmlGraph />
<GraphContext.Provider
value={{
option,
setOption,
}}
>
<Layout>
<Layout>
<Header>
<GraphToolbar
></GraphToolbar>
</Header>
<div>
<SysmlGraph setGraph={setGraphInstance} />
</div>
</Layout>
<Sider>
<GraphSiderHeader></GraphSiderHeader>
{option === 'lib' && <SysmlStencil />}
{option === 'page' && <GraphSiderPageStyle></GraphSiderPageStyle>}
{option === 'style' && <GraphSiderGraphStyle></GraphSiderGraphStyle>}
</Sider>
</Layout>
</GraphContext.Provider>
在React中,每当组件重新渲染时,其内部的所有代码,包括函数的定义和状态的创建,都会被重新执行,这也包括常量函数的定义,这也就解释了为什么常量函数updateGraph
作为上述useEffect
依赖时,会因为非父子组件侧边栏的切换而导致useEffect
不必要的执行
解决
使用useCallback
,会在组件挂载时创建一个稳定的函数引用,并且在依赖项不变的情况下保持相同的引用,这样做可以避免不必要的函数重复创建,提高性能。
即如果updateGraph
的实现和逻辑在组件的生命周期中保持不变,并且希望在组件的重新渲染时保持相同的函数引用,useCallback
可以提高性能
//更新画布发送请求后端保存
const updateGraph = useCallback(
(cell: Cell) => {
//生成虚拟边不保存
if (cell && cell.id === 'dummy-edge') return;
updateSysML({ id: graphId, cells: graph.toJSON().cells }).then(res => {
if (res.code !== 200) {
messageApi.error(res.msg);
}
});
},
[graphId, messageApi],
);
小结
比较
常量函数适用于简单的函数逻辑,不需要在组件重新渲染时保持相同引用的情况。(例如:响应点击事件的函数,不需要在useEffect
里调用)
而useCallback
适用于需要优化性能的情况,特别是当将函数作为回调传递给子组件时,可以使用useCallback
来确保子组件在依赖项不变的情况下不会重新渲染。
内存泄漏
如果不将常量函数updateGraph
加入到useEffect
依赖项中时,按照正常的思维逻辑,useEffect
不依赖与常量函数的变化,并不会导致不必要的执行。
从效果上来看这样写是不会出现逻辑上的问题,但由于缺乏依赖项,仍会导致其他隐性的问题:
在
useEffect
中使用常量函数时,常量函数会捕获组件作用域中的变量,形成闭包,即内部引用了组件作用域中的某些变量。当组件重新渲染时,常量函数会重新被创建,由于其没有被添加到依赖数组中,
useEffect
会在每次组件重新渲染时继续引用前一个渲染周期的常量函数。由于该常量函数包含对就组件作用域中的变量引用(即形成了闭包),这将导致旧的变量一直保持活动状态,无法被垃圾回收,即时组件被销毁,它们也会继续占用内存,导致内存泄漏
0 条评论