前沿
这两天在集成一个比较复杂的组件,大致的内容是有个list组件,然后渲染每个item组件,这个item组件是比较重型,并且不太好轻易调整。在集成的过程中遇到了性能问题,我们来分析下,看下怎么解决。我简化了下,做了个简单的案例。
案例说明
重型的ComplexItem组件
export interface IComplexItemProps {
id: string;
title: string;
onRemove: (id) => void;
onChange: (data: any) => void;
// 还有很多props
[x: string]: any;
}
export const ComplexItem = (props: IComplexItemProps) => {
const { title, onRemove, id, onChange } = props;
const handleClick = () => {
onRemove(id);
};
const handleChange = (e: any) => {
onChange({id: id,title: e.target.value });
};
return (
<div className="item">
<Input value={title} onChange={handleChange} />
<Button type="primary" style={{ marginLeft: 10 }} onClick={handleClick}>
删除
</Button>
</div>
);
我们需要封装该组件,做成一个列表。
封装ComplexItem组件
这里我们用业务Item组件,去封装ComplexItem组件,组件内容如下
export interface IItemProps {
id: string;
title: string;
onRemove: (id) => void;
onChange: (data: any) => void;
}
export const Item = React.memo(
(props: IItemProps) => {
// 这里会组合大量的其它props
const otherProps = useMemo(() => {}, []);
console.log('item-render', props.id);
return <ComplexItem {...props} {...otherProps} />;
}
);
这里我们使用memo对组件包裹,避免组件重新渲染。
List组件,渲染Item组件
export const List = (props) => {
const [data, setData] = useState(initData);
const handleChange = ({ index, value }) => {
setData((prevItems) => {
const newItems = [...prevItems];
newItems.splice(index, 1, value);
return newItems;
});
};
const handleDelete = (index) => {
setData((prevItems) => {
const newItems = [...prevItems];
newItems.splice(index, 1);
return newItems;
});
};
return (
<div>
{data?.map((item, index) => {
return (
<Item
key={item.id}
id={item.id}
title={item.title}
onRemove={() => handleDelete(index)}
onChange={(value) => handleChange({index, value})}
/>
);
})}
</div>
);
这里就是构造数据,渲染Item,Item组件内容变更做更新和删除。
性能问题
我们通过上面的案例做测试,如果改变其中ComplexItem
一个组件中的input值,发现,所有的子组件都会重新渲染,上面我们已经对Item
组件做了React.memo
包裹,为啥还会重新渲染呢,仔细查找,不难发现
onRemove={() => handleDelete(index)}
onChange={(value) => handleChange({index, value})}
是这两个方法每次渲染都会重新生成一个新的函数,导致子组件会重新渲染。
通过上面的代码,我们看到,因为我们的handlexxx
方法需要使用当前的索引,所以只能在渲染的时候动态生成一个方法,
性能解决方案
在不调整子组件代码的情况下,看看怎么解决这个问题。
handlexxx
返回一个不变的函数
我们想到,如果回调返回一个不变的函数,这个问题就解决了。于是打算封装一个hooks来实现该功能。这个hooks起名为useReturnMemoizedFn
.内容如下,大家可以先不看下面的代码,看看能不能自己写出来
import { useEffect, useState, useRef } from 'react';
type noop = (this: any, ...args: any[]) => any;
type GeneratorParams = any;
export function useReturnMemoizedFn(
callback: (generatorParams: GeneratorParams, ...innerArgs: Array<any>) => void
) {
// 存放函数map
const currIndexRef = useRef(new Map());
// 生成一个不变的函数
const generatorFun = (key: string) => {
const isExists = currIndexRef.current.has(key);
if (!isExists) {
currIndexRef.current.set(key, (...args: any[]) => {
const gParams = JSON.parse(key);
callback(gParams, ...args);
});
}
return currIndexRef.current.get(key);
};
// 生成回调要执行的函数,同时保留参数
return (generatorParams: GeneratorParams) => {
// 根据参数生成key
const key = JSON.stringify(generatorParams);
return generatorFun(key);
};
}
使用如下
const generatorChangeFun = useReturnMemoizedFn(({ index, a }, value) => {
console.log('eee', { index, a }, value);
handleChange({
index,
value,
});
});
const generatorDeleteFun = useReturnMemoizedFn(({ index }) => {
handleDelete(index);
});
return (
<div>
{data?.map((item, index) => {
return (
<Item
key={item.id}
id={item.id}
title={item.title}
onRemove={generatorDeleteFun({ index })}
onChange={generatorChangeFun({ index, test: 's' })}
/>
);
})}
</div>
通过以上,问题得以解决。
子组件通过第二个参数排除可变函数
如果改变子组件的情况下,我们可以通过memo的第二个参数把onChange这类的方法排除掉,这会有个问题,如果后续加了其它方法,还要记得做排除,要不性能问题又来了。
export const Item = React.memo(
(props: IItemProps) => {
// 这里会组合大量的其它props
const otherProps = useMemo(() => {}, []);
console.log('item-render', props.id);
return <ComplexItem {...props} {...otherProps} />;
},
(prevProps, nextProps) => {
// 自行实现比较规则
const excludeProps = ['onChange', 'onRemove'];
const prevPropsResult = _.omit(prevProps, excludeProps);
const nextPropsResult = _.omit(nextProps, excludeProps);
return _.isEqual(prevPropsResult, nextPropsResult);
}
);
改props,让index也作为props
这种也是需要改动item组件,加一个index的props,在每个事件中把index参数给带上,这个我就不实现了,比较简单,大家可以自行尝试。
结束语
通过把 handlexxx
返回一个不变的函数,这个大家看看有没有更优雅的方案,可以发到评论区,一起学习。
整个案例在 https://stackblitz.com/edit/vitejs-vite-bgrprg?file=src%2FApp.tsx, 可以自行fork,尝试
如果你觉得该文章不错,不妨
1、点赞,让更多的人也能看到这篇内容
2、关注我,让我们成为长期关系
3、关注公众号「前端有话说」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章