首先上代码
Child.jsx
import React from "react";
interface itemProps {
add: Function,
val: number
}
function areEqual(prevProps: itemProps, nextProps: itemProps) {
return Object.is(prevProps.val, nextProps.val);
}
function Child(props: itemProps) {
const {add, val} = props;
console.log("render Child");
return (
<div onClick={() => add()}>
{val}
</div>
)
}
export default React.memo(Child, areEqual);
Father.jsx
import React, {useState} from 'react';
import Child from "./Child";
export interface Box {
val: number,
}
const cartData = Array.from({length: 5}, (v, val) => val);
function Index() {
const [counts, setCounts] = useState<number[]>(cartData);
const add = function (index: number) {
// 深拷贝一份
const target = counts.slice(0);
target[index] = target[index] + 1;
setCounts(target);
};
console.log("render Test.js");
return (
<div className="Test">
{
counts.map((val, index) => (
<Child key={index} val={val} add={() => add(index)}/>
))
}
</div>
);
}
export default Index;
分析代码
场景很简单,Father组件内部有多个Child组件,点击某个Child组件,对应的数组元素+1。
这里用到了React.memo,React.memo() 和 PureComponent 一样,用于判断组件是否需要更新,这里不再赘述。
该场景下, Father组件的counts发生改变时,首先自身重绘,然后是Child组件,Child经过memo包裹后,只有val发生改变后才会重绘,而剩余的组件会返回上一次的组件渲染结果,
再看add方法
const add = function (index: number) {
// 深拷贝一份
const target = counts.slice(0);
target[index] = target[index] + 1;
setCounts(target);
};
当我们点击第index个Child时,首先数组counts发生改变,引起Father组件重绘,然后第index个Child组件也要重绘,看着是没有问题对吧。
来看结果
我们想要的结果
现实中的结果
我们看到,点击某个下标后,对应的数值确实发生改变了,但是我们再点击其他下标时,counts的数据貌似是又变成了最初的状态。
Child组件唯一和counts关联的地方就是add方法,我们来看add方法做了哪些事呢。
首先深拷贝counts赋值给一个常量,然后找到counts的index下标,对应值+1,最后调用setCounts改变counts。
逻辑上来讲没有问题。
但是细心的小伙伴有没有发现,add方法内部是操作了数组counts的【值】
而且react hook实际上是一个闭包,每次渲染的state都是最新,那么出现这个问题,首先我们就想到,或许是脏数据导致。
我们再来看问题,第index个Child组件重新渲染,组件闭包执行,返回当前最新状态的Child组件,
- 对于剩余的没有更新的的组件,则直接返回上一次的渲染结果(包括函数式组件内部的所有变量/常量、方法,以及最后的渲染结果)。
- add方法内部是操作了counts的值,而不是地址。
那么我们就想到了,这是读取到了脏数据。
解决方式
解决方式:址引用 替代 值引用
const add = function (index: number) {
setCounts(val => {
return val.map((item, i) => {
if (i === 0) {
item++;
}
return item;
})
});
};