前言
如今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的开发,也可以不至于useState
,useEffect
,useRef
,开发组件的时候,可以考虑包一层memo
,减少资源的损耗;有些不需要频繁变动的方法也可以加一层useCallback
其他
而在这种交互方案上,我们也可以考虑采用其他的一些方式来提高性能。比如像ant design
在table的示例中给的一个demo:table-可编辑单元格。
这种方案可以满足大多数需求。在Input内部维护一个state,只有在失焦的时候才触发onChange事件,外部只需要感知数据的变化,而不需要再次render整个列表,onBlur事件会在onClick事件之前触发,所以在触发提交之前,会先触发onChange,从而拿到最新的数据。