React 渲染优化,你有更好的方案吗

前沿

这两天在集成一个比较复杂的组件,大致的内容是有个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组件内容变更做更新和删除。

image.png

性能问题

我们通过上面的案例做测试,如果改变其中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、关注公众号「前端有话说」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值