React - 针对表格中输入框性能优化思路

3,186 阅读6分钟

前言

如今React在开发者中越来越普及,组件化的思想极大地提高了我们的开发效率。但在开发的时候,由于需求快速迭代,且急忙上线,我们可能较少地考虑React的一些性能问题,这篇文章主要讲一个自己的需求优化经验

需求背景

接到一个在代码层面看不合理,但实际使用上确实很合理的交互,即将一整个 table 做一个大的表单,用户可以直接在里面进行输入,最后再进行提交。大概如下:

这样的交互对用户操作的方便性确实会提高很多,既然需求来了,那咱就开始做呗

性能问题

先把demo扔出来
基于ant-design开发的,没有用到form,provider等其他的一些东西,单纯地通过更改list中的数据,从而同步input中的数据。

先看没有做处理的表格,在这里笔者先 mock 100条数据:

const [data, setData] = useState(originData);
const onDataChange = (key: string) => {
    return (value: string) => {
      setData([...data.map((i) => (i.key === key ? { ...i, value } : i))]);
    };
  };
// table columns
const columns = [
    ...,
    {
      title: "operation",
      dataIndex: "operation",
      render: (_: any, record: Item) => {
        return (
          <Input onChange={onDataChange(record.key)} value={record.value} />
        );
      }
    }
  ];

这里为了方便看到render的效果,笔者给Input包一层,打上console

import React from "react";
import { Input as AInput } from "antd";

interface IInputV2 {
  index: string;
  value?: string;
  onChange: (index: string, v: string) => void;
}
export const Input = React.memo((props: IInput) => {
  console.log("input render");
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    props.onChange(e.target.value);
  };
  return <AInput onChange={handleChange} value={props.value} />;
});

在下面的gif中,可以看到每输入一个值,整个table会render一次,而我们在input组件里面打了log,就打印了100次,相当于这100条数据中的input框都重新render了一遍。笔者输入了6个数字,可以看到控制台有600个log

笔者当时遇到的情况会更复杂些,还需要与其他单元格进行交互。demo中的数据量还行,且没有很复杂,所以输入的时候也不会感觉到有明显的卡顿,但同样也是对浏览器资源的一种损耗。

memo

官方介绍在这:React.memo 简单讲就是,被memo分装过的组件,会对组件传入props进行浅比较,如果props中的值没发生改变,那么这个时候,这个组件也就不会rerender。

细心的朋友会发现,其实上面的代码中,笔者是做了一层memo的,但为什么没生效,原因就在于,onDataChange这个方法中返回的这个箭头函数是没有固定指针的,甚至连onDataChange这个方法本身都是没有固定指针的,每一个setData重新render的时候,方法都会生成一个新的地址,导致props发生了变化。那么这个时候我们就要开始来做改造了

const onDataChange = (key: string) => {
    return (value: string) => {
      setData([...data.map((i) => (i.key === key ? { ...i, value } : i))]);
    };
  };

之所以在onDataChange返回一个function,是因为一开始笔者是期望能在外面就把对应input的index给拿到,用于找第几个input框的值改变,保存下来,但这样传进去的function就没法固定,因而我们把index值当做props传给Input,再由Input回传回来

interface IInputV2 {
  index: string;
  value?: string;
  onChange: (index: string, v: string) => void;
}
export const InputV2 = React.memo((props: IInputV2) => {
  console.log("inputV2 render");
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    props.onChange(props.index, e.target.value);
  };
  return <AInput onChange={handleChange} value={props.value} />;
});

这样就不需要将一个不确定的匿名函数传入到组件中,接下来就是怎么去固定传进去的onChange方法,这里有两种思路

  • Class Component
  • useCallback

Class Component

class ClassTable extends React.Component<{}, { data: Item[] }> {
   constructor(props: any) {
   ...
    this.columns = [
     ...,
      {
        title: "operation",
        dataIndex: "operation",
        render: (_: any, record: Item) => {
          return (
            <InputV2
              index={record.key}
              onChange={this.onDataChange}
              value={record.value}
            />
          );
        }
      }
    ];
  }
  onDataChange = (key: string, value: string) => {
    this.setState({
      data: [
        ...this.state.data.map((i) => (i.key === key ? { ...i, value } : i))
      ]
    });
  };
  ...
}

Class Component就比较直观了,用this来调用的function指针固定

useCallback

用React Hooks开发的同学或多或少接触过这个方法,官方介绍。简单讲就是将方法地址记录下来,只在传入的第二个依赖参数数组改变的时候,才做更新。

但这里面有个小坑,往下看代码.因为一开始有个定向思维,setData的时候,一般传的都是一个最终的值,而这个终值是依赖data本身,所以有需要将data作为参数传进去,这样又会更新function的地址,这样就跟没有用useCallback一样,导致一开始的时候笔者会认为Hooks没有Class Component好用。

/** bad */
  const onDataChange = useCallback((key: string, value: string) => {
    setData([...data.map((i) => (i.key === key ? { ...i, value } : i))]);
  }, [data]);

但实际上,Hooks是有提供另外的解决方法的,setData的时候,是可以传入一个function,而function的入参就是最新的data值,这样的话,就不需要把data作为依赖传到useCallback

/** good */
  const onDataChange = useCallback((key: string, value: string) => {
    setData((preData) => preData.map((i) => (i.key === key ? { ...i, value } : i)));
  }, []);

效果如下,可以看到,每输入一个数字,只有对应的对应的Input组件render了:

setData过程中,需要变的元素有哪些

可以参考下图,在上面的案例中,要触发整个table的render,就必然需要改变最外层数组的地址,也就是根节点要发生改变。其次是叶子节点的value值也要改,才能set到输入框的值。而正常来讲,连接根节点和叶子节点中间的一些节点也需要发生改变,但需要根据具体的代码设计,像代码示例中,因为table内部的处理没有,可以不用改变

总结

本文并不提供开发方案,只是在开发中针对性能的一种优化思路,在开发的时候可以找到很多种解决方案,下面我也会加了另一种交互方案,从而使我们处理起来更加合理方便。而针对React Hooks的开发,也可以不至于useStateuseEffectuseRef,开发组件的时候,可以考虑包一层memo,减少资源的损耗;有些不需要频繁变动的方法也可以加一层useCallback

其他

而在这种交互方案上,我们也可以考虑采用其他的一些方式来提高性能。比如像ant design在table的示例中给的一个demo:table-可编辑单元格

这种方案可以满足大多数需求。在Input内部维护一个state,只有在失焦的时候才触发onChange事件,外部只需要感知数据的变化,而不需要再次render整个列表,onBlur事件会在onClick事件之前触发,所以在触发提交之前,会先触发onChange,从而拿到最新的数据。