React Hook 一定会用到的属性是 useState,对于 state 的设置和获取在 Hook 的体现就是 useState

基本用法不多重复:

const [count, setCount] = useState(0);

这里测试了几种使用情况,以及 hook function component 和 class component 的几种表现的区别。

场景1、先 setState 然后通过 setTimeout 取值

这个场景中,通过一个方法,每次先 setState,然后通过 setTimeout 取 state,在 class component 的实现中,取了两个值,一个是方法内部作用域的值 count,一个是 this.state.count

class component 的写法:

addHadnleTimeout2 是一个 click 事件处理方法

    addHandleTimeout2 = () => {
        const { count } = this.state;
        console.log(`----timeout count ---- ${count}`)
        this.setCount(count + 1);

        setTimeout(() => {
            console.log(`----this.state.count---- ${this.state.count}`);
            console.log(`----count---- ${count}`);
        }, 2000);
    }

console 结果:

35332-wb2f7fpmxgo.png

hook function component

    const addHandleTimeout2 = () => {
        console.log(`----timeout count ---- ${count}`)
        setCount(count + 1);
        setTimeout(() => {
            console.log(`----count---- ${count}`);
        }, 2000);
    }

console 结果

45421-ighutudcapq.png

区别点:

可以发现,当我在 setTimeout 中分别 console.log(count)(function component) 和 console.log(this.state.count)(class component) 的时候,两者输出的结果是不同的。

在 function component 中,setTimeout 的输出分别是:0、1、2、3

在 class component 中,setTimeout 每次输出分别是:4、4、4、4

从代码理解的角度来讲,两者行为应该是一致的,不一致的原因需要从两者角度去理解:

以下引自:https://juejin.im/post/5ceb36dd51882530be7b1585

首先是对 class component 的解释:

  • state 是 Immutable 的,setState 后一定会生成一个全新的 state 引用
  • 但 Class Component 通过 this.state 方式读取 state,这导致了每次代码执行都会拿到最新的 state 引用,所以快速点击4次的结果是 4 4 4 4

然后是对 function component useState 的解释:

  • useState 产生的数据也是 Immutable 的,通过数组第二个参数 Set 一个新值后,原来的值在下次渲染时会形成一个新的引用
  • 但由于对 state 的读取没有通过 this. 的方式,使得 每次 setTimeout 都读取了当时渲染闭包环境的数据,虽然最新的值跟着最新的渲染变了,但旧的渲染里,状态依然是旧值

为了理解这个渲染闭包环境,我们断点看下 function component 中这个方法每次取到的值:

第1次:

22074-hviay8ax2km.png

第2次:

02481-q5f6r45qzg.png

第3次:

01020-ole4r5cd7a.png

上面的三次断点中,每次这个方法中 count 都是方法一进来时拿到的 count,所以即使延迟 2S 去取 count 取到的也是上次的结果

相比于上面的结果,如果在 class component 中断点:

第1次:

18411-daedhqybt7.png

第2次:

86060-d0ix0urbrlw.png

其他的以此类推

所以,其实在每次的渲染中,方法中拿到的 count 变量其实是上次的 state 的结果,但是 this.state.count 每次都会拿到最新的 state 的引用,所以挂载的属性 this.state.count 在 2s 之后拿到的最新的。

场景2、对象或数组一级属性 set 的表现

首先声明:下面的代码中,无论是 setArrayHandle 还是 setObjectHandle 都是一种不规范的写法,理论上我们都应该创建一个新的引用,而不是在之前的引用中直接修改属性

function component 中直接修改 state 属性,然后进行 set

  const setArrayHandle = () => {
        // const newList = Array.from(list);
        const newList = list;
        console.log(newList === list);
        console.log(`---before--- ${list}`);
        newList[0] = Math.floor(Math.random() * 10);
        setList(newList);
    }

    const setObjectHandle = () => {
        const newObj = obj;
        // const newObj = Object.assign({}, obj);
        console.log(newObj === obj);
        newObj.name = Math.floor(Math.random() * 10);
        setObj(newObj);
    }

为了监听 state 的变化,使用了两个 useEffect:


    useEffect(() => {
        console.log(`----effect--- ${JSON.stringify(obj)}`);
    }, [obj]);

    useEffect(() => {
        console.log(`----effect--- ${JSON.stringify(list)}`);
    }, [list]);

每次点击按钮的时候,都会触发其中的方法,然后直接修改了 state 的一个属性,这种用法其实是有问题的,虽然我把 obj 这个 state 赋值给了 newObj,但其实引用还是同一份。

这个时候,去 setObj(newObj) 会发现,其实页面根本没有触发重新渲染。

GIF222.gif

上面的图示中,左边是上面的代码示例触发的点击,可以发现,每次通过 console 输出的时候值都是变化的,但并没有触发模板的 re-render,effect 也没有触发。

而右边其实是我在 class component 直接修改了 this.state.obj.name=xxx,然后去 setState,当然了 this.state.arr[0]=xxx 也是一样的修改逻辑

    setArrayHandle = () => {
        const newList = this.state.list;
        console.log(newList === this.state.list);
        console.log(`---before--- ${this.state.list}`);
        this.state.list[0] = Math.floor(Math.random() * 10);
        this.setState({
            list: this.state.list
        }, () => {
            console.log(`---after--- ${this.state.list}`);
        });
    }

    setObjectHandle = () => {
        const newObj = this.state.obj;
        console.log(newObj === this.state.obj);
        console.log(`---before--- ${JSON.stringify(this.state.obj)}`);
        this.state.obj.name = Math.floor(Math.random() * 10);
        this.setState({ obj: this.state.obj });
        console.log(`---after--- ${JSON.stringify(this.state.obj)}`);
    }

再次声明,上面的写法是错误的

从这两次对比中可以发现,在 Hook 中直接修改 state 的一个对象(或数组)属性的某个子属性或值,然后直接进行 set,不会触发重新渲染

在 class component ,setState 之后无论传给方法的是个什么值,都会触发重新渲染

class component 这个不过多解释,这个没有什么疑义,关键是 function component,通过 useState 这个 hook 实现的方式上的表现

问题:是否可以认为,react hool setState 默认实现了依次类似于 PureComponent 的逻辑呢?

断点下来,可以发现走到了这里:

82005-5q0nw3lddjd.png

这里我们发现的是有一个对比,发现 eagerStatecurrentState 其实是相同的,因为本身我们就是修改的 state 的 obj.name,因此在这次闭包中,认为传过来的新的 state 其实和之前对比是相同的(之前的 state 是我们人工修改的值),这种情况下,就不会出发渲染

这个逻辑其实和 class component 中使用 PureComponent 差不多,使用 PureComponent 其实可以复现我们上面的情况

这里我们使用 PureComponent 代码实现的相同逻辑:

    setObjectHandle = () => {
        const newObj = this.state.obj;
        console.log(newObj === this.state.obj);
        console.log(`---before--- ${JSON.stringify(this.state.obj)}`);
        this.state.obj.name = Math.floor(Math.random() * 10);
        this.setState({ obj: this.state.obj });
        console.log(`---after--- ${JSON.stringify(this.state.obj)}`);
    }

效果如下:

3.gif

其实我们可以发现,这个场景中我们也是直接修改了 this.state.obj.name = xxx,在 setState 中,当我们把 this.state.obj 丢进去的时候,react 会将 之前的 this.state 和 传入的 this.state 进行浅比较,此时肯定是相等的,也不会触发重新渲染。

通过下面三个断点可以发现,这种场景下,setState 的时候,内存中 this.state.obj 和目标更新的 this.state.obj 浅比较结果其实是一样的。

因此不会触发更新

92559-wgpn2vngjvr.png

78080-x385qdtf4fq.png

84782-gzw4iem3ubu.png

代码

相关代码可以在:https://github.com/postbird/react-hook-practice 这个仓库中找到