React.memo引发了脏数据,究其原因竟是因为这个。。。

664 阅读2分钟

首先上代码

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组件,

  1. 对于剩余的没有更新的的组件,则直接返回上一次的渲染结果(包括函数式组件内部的所有变量/常量、方法,以及最后的渲染结果)。
  2. add方法内部是操作了counts的值,而不是地址。

那么我们就想到了,这是读取到了脏数据。

解决方式

解决方式:址引用 替代 值引用

const add = function (index: number) {
    setCounts(val => {
        return val.map((item, i) => {
            if (i === 0) {
                item++;
            }
            return item;
        })
    });
};