场景:如果你有一种列表,需要更改列表里面的每一项数据,当你点击每一项的时候,改变当前数据项的时候,可能需要看一看这篇文章。
测试的功能:当点击每一项的时候,这一项的数据+1。
先看最先的写法:
export default function Test() {
const [data, setData] = useState(new Array(1000).fill(1))
const onClick = index => {
data[index] += 1;
setData([...data]);
}
return (
<FlatList
data={data}
renderItem={({ item, index }) => <TouchableOpacity onPress={_ => onClick(index)} key={index}>
<Text>{item}</Text>
<Text>点击</Text>
</TouchableOpacity>}
/>
)
}
这里我测试1000个数据的情况,其实对于FlatList来说数据量在一定的时候,多少来说都差不多。FlatList并不会自动将全部数据渲染出来。这个例子中,当我点击每一项的时候,差不多1s才更新值。
这个速度我相信很多人都是接受不了的,一般用户在点击的时候,如果超过500ms,都是很明显的察觉的,而且超过这个值的时候大部分会被定义为卡顿,所以我们需要将其提升到用户可以接受的程度。接下来我会将分析,边更换代码进行测试,最后达到相对最优。
以现在的编写的方式我们知道,当一项数据发生改变的时候,全部数据都要刷新,幸好有FlatList,只会刷新已经渲染的部分,没有渲染出来的不会刷新,所以,即便你把数据改到10000级别也是一样的。 其实我们非常清楚,我们点击其实只需要更新当前项即可,要想达到这个目标,我们需要明白的是,我们的刷新都是直接通过react来进行的,而react刷新都是由当前组件刷新到当前组件的子组件,比如这个需要先刷新FlatList,然后再刷新每一项,如果我们使用上面的方式,也就是当这个组件刷新,那么这个组件及其子组件都会刷新,如果我们能够控制当某一项的值改变的时候这个子组件刷新,其他的组件就不刷新,那么我们就需要将Item的渲染独立出去,让这个子组件有自己的渲染的条件,也就是我有权自己确定我自己的渲染。
export default function Test() {
const [data, setData] = useState(new Array(300).fill(1))
const onClick = index => {
data[index] += 1;
setData([...data]);
}
function renderItem({ item, index }) {
return <RenderItem onClick={onClick} index={index} item={item} />;
}
return (
<FlatList
data={data}
renderItem={renderItem}
/>
)
}
其中RenderItem组件被我封装了,渲染的内容跟上面一样。我没有做任何处理的时候,发现渲染仍然很慢,经过我打印信息发现,当我点击的时候,每一个item仍然都重复渲染渲染了。我们知道,当props只有改变的时候才会刷新,也就是onClick,index,item,当我点击的时候,除了当前的项的item发生改变,其他的不变,但是事实并不是我说的这样,而是onClick一直是不相等的,所以一直会刷新。既然发现了原因,那么就知道怎么改了:因为当前组件刷新了,这个函数组件就会重新执行,也就是函数onClick会被重新被定义,故而每一次点击的时候刷新了,导致重新定义,进而导致每一次比较都不相等。我就将函数移到这个函数组件移到外面。 我放到外面发现就需要将data, setData这两个传入子组件中,我们知道data在点击的时候一定要变化,不然不会刷新。于是我尝试使用缓存函数的方式。
function cloneFunc() {
let func = null;
return (funcName) => {
if (!func) {
console.log("进来了")
func = funcName;
}
return func;
}
}
let func = cloneFunc();
let test = null;
export default function Test() {
const [data, setData] = useState(new Array(300).fill(1))
const onClick = index => {
data[index] += 1;
setData([...data]);
}
function renderItem({ item, index }) {
return <RenderItem onClick={func(onClick)} index={index} item={item} />;
}
return (
<FlatList
data={data}
renderItem={renderItem}
/>
)
}
这样就好多了。
但是要是都这样写,岂不是很麻烦,我们在函数组件中使用函数的时候,可以使用useCallback包裹。为啥在函数组件中函数的定义会被重复定义,就是因为每次渲染的时候都会函数都会被重新执行,所以在函数组件中使用的函数和变量都会重新定义,这里也说道局部变量,不能使用常用的方式定义变量,而是react提供的API,或者将变量定义在函数外部。使用useCallback改造上面函数。
export default function Test() {
const [data, setData] = useState(new Array(1000).fill(1))
const onClick = useCallback(index => {
data[index] += 1;
setData([...data]);
}, []);
function renderItem({ item, index }) {
return <RenderItem onClick={onClick} index={index} item={item} />;
}
return (
<FlatList
data={data}
renderItem={renderItem}
/>
)
}
写到这里是不是已经是最好了,可以说这样大部分需要都可以满足了,但是我想做到真正的让每一个Item都有自己的行为,也就是我自己控制我的值,而不是由父级控制,我们知道即便通过上面的控制,就可以避免大部分渲染,但是父级仍然会调用那么多次,而我想要的是当点击的是Item自己处理,而父级只负责保存由Item改变的值。
class RenderItem extends React.PureComponent {
state = {
item: this.props.item
}
onChangeText = _ => {
const { changeItem, index } = this.props;
let { item } = this.state;
this.setState({ item: item + 1 }, _ => {
changeItem(index, this.state.item);
});
}
render() {
const { index } = this.props;
const { item } = this.state;
return (<TouchableOpacity onPress={this.onChangeText} key={index}>
<Text>{item}</Text>
<Text>点击</Text>
</TouchableOpacity>);
}
}
export default function Test() {
const changeItem = useCallback((index, item) => {
testData[index] = item;
}, []);
function renderItem({ item, index }) {
return <RenderItem changeItem={changeItem} index={index} item={item} key={index} />;
}
return (
<FlatList
data={testData}
renderItem={renderItem}
/>
)
}
经过这样改动,速度快了很多,我要说明的是,原本RenderItem组件也使用HOOKS的,结果因为我使用不好,总是没有class的方式速度快,所以我换成了类组件。
总体的思路是父级只负责保存数据,不负责渲染,而是由子组件自己负责自己的渲染。所以在子组件中在改变值的时候也同样要告诉父组件自己变了,同时也要说明的是,应该在渲染结束后告诉父级改变数据,毕竟用户以自己看到的数据为准,并不是我们保存的数据为准。
两种思路可以理解为,第一种是中央集权,第二种就是在第一种的基本上放权,但是大部分人都推荐使用第一种,因为我们希望有一个可以统一管理的。而第二种就像中央发布指令,就告诉下边的人只要你做了,调用我给你的函数,其余你怎么做你自己做主。其实数据仍然是自己控制,只是不再控制怎么渲染。
经过这段时间我对hooks的学习,写出了我觉得关于本实例最好的代码:
function RenderItemHook({ propsItem, changeItem, index }) {
const [item, setItem] = React.useState(propsItem)
const onChangeText = React.useCallback(() => {
setItem(item + 1)
changeItem(index, item + 1);
}, [item])
return React.useMemo(() => {
return (
<TouchableOpacity onPress={onChangeText} key={index}>
<Text>{item}</Text>
<Text>点击</Text>
</TouchableOpacity>
)
}, [item])
}
发现速度仍然比不过类组件,在列表组件渲染结束的时候点击是一样的,但是还在渲染的过程中如果点击的话,仍然很慢,很卡,所以就目前来说我推荐类组件的方式完成子组件。