本文是在:https://juejin.im/post/5ceb36dd51882530be7b1585#heading-13 的基础上进行的探究,非常建议阅读原文

之前写过一篇文章,setTimeout 在 function component 和 class component 中读取 state 值的表现是不同的,并解释了这种不同的原因以及如何实现读取最新值。
文章地址:http://www.ptbird.cn/react-hook-usestate-setState.html

一、useRef 在渲染闭包内总是读取最新值

在一次渲染闭包内,如果我们想每次都读取最新的,首先我们需要将值挂到一个 object(模拟 class component 的 this.state.xxx)

比如下面代码中:

    const addHandle = () => {
        setCount(count + 1);
        setTimeout(() => {
            console.log(`count is : ${count}`);
        }, 1000);
    }

如果点击三次,输出结果是:

33455-edkebx35zs8.png

每次渲染比包中,我们拿到的 count 都是当前值(当前值就是 0 - 1 - 2),count 的当前值没有到 3

为了每次都拿到最新值,通过一个 ref 并且将数据都挂载到 ref.current 上面

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

    const addHandle = () => {
        setCount(count + 1);
        countRef.current = count + 1;
        setTimeout(() => {
            console.log(`count is : ${count}`);
            console.log(`countRef.current is : ${countRef.current}`);
        }, 1000);
    }

这样每次点击实际上都是直接取的 countRef.current

42335-6isf9wrdjff.png

不过我们可以发现,上面这种写法会导致我们每次都要显示的写一行代码 countRef.current = count + 1; ,这样非常不优雅。

为了解决这个情况,我们希望的就是当 count 变化的时候,能够自动的给 countRef.current 赋值。

因此可以使用 uesEffect 去解决

二、使用 useEffect 配合 useRef 在渲染闭包中总是获取最新值

useEffect 是用来处理副作用的,在每次的 render 完毕之后都会重新执行

从理解上来说,每次的 render 都会执行一次声明的 uesEffect ,但是具体的时机其实是在 DOM 操作完毕之后

上面的 useRef 的实现中,每次我们都会显示的使用 countRef.current = count + 1; 这个代码,我们的目的是实现能够自动给 countRef.current 赋值的方式。

因为每次 useEffect 都会在 render DOM 操作完之后执行,因此我们可以借助 useEffect 的这个能力,每次点击之后,触发一次重新渲染,然后在这里面通过 useEffect 给 countRef.current 赋值。

具体代码如下:

    useEffect(() => {
        countRef2.current = count;
        console.log('useEffect')
    });

    console.log('-----render-----')

    const addHandle = () => {
        setCount(count + 1);
        setTimeout(() => {
            console.log(`count is : ${count}`);
            console.log(`countRef2.current is: ${countRef2.current}`);
        }, 1000);
    }

上面的代码中,在 function component 中写了一个 console.log('render') ,然后在一个 useEffect 中写了 console.log

并且这个 uesEffect 没有依赖任何数据的变化(useEffect 的第二个方法未传递)

  • 页面初始化未点击:

当页面初始化完成后,会首先触发一次,console 和 useEffect:

虽然我刻意将 useEffect 放在了 console.log() 上面,但是 useEffect 还是会在 console 之后在执行,这就是 useEffect 实际上执行的时机是在页面 render 之后

95363-ya5w3uuj49c.png

  • 第 1 次点击

第一次点击之后,还是会首先执行一次 render 字符串的输出,然后执行 useEffect

不同的地方在于在方法中 console.log(currentRef.count) 的时候,拿到的已经是最新的值,这就说明,uesEffect 已经在 render 操作完成执行。

78291-qns9mkxqa2.png

  • 快速点击3次

继续快速点击 3 次,首先每次都会执行 render 字符串输出以及 effect 的执行,并不会等待 setTImeout 相关的执行,在 setTimeout 中输出的时候,因为已经执行了 useEffect ,对象已经更新,因此我们是可以拿到最新的 state

07687-k65jdv6fg4o.png

三、自定义 hook 优雅实现获取最新 state

上面的代码中,虽然解决了显示代码 countRef.current=xxx这个代码调用问题,但是这行代码还是出现在了我们的 component 中

能否不在 component 中出现这个代码以及不出 countRef 的声明?这时候需要我们去封装这个能力,React 提供了自定义 hook 的方案。

自定义 hook 自己可以封装一些能力,并且将最终需要的数据暴露出来,比如上面的代码中, useEffect 和 useRef 其实是可以封装到一个方法里面去的。

下面这个方法中,传入一个 value 并且监听 value 的变化,将值传入到 ref.current 中,实现上面的功能,最终将 ref 这个对象暴露出来:

const useCurrentValue = (val) => {
    const ref = useRef(val);
    useEffect(() => {
        ref.current = val;
    }, [val]);
    return ref
}

这样每次只需要使用一个变量初始化就可以,变量值变化,对象的属性值都在变化。此时,组件代码如下:

const useCurrentValue = (val) => {
    const ref = useRef(val);
    useEffect(() => {
        ref.current = val;
    }, [val]);
    return ref
}

export default (props = {}) => {
    const [count, setCount] = useState(0);

    const countRef3 = useCurrentValue(count);
    const addHandle = () => {
        setCount(count + 1);
        setTimeout(() => {
            console.log(`countRef3.current is: ${countRef3.current}`);
        }, 1000);
    }

    return (
        <div>
            <h2>function component</h2>
            <p>count is {count}</p>
            <button onClick={addHandle}>add</button>
        </div>
    );
}

初始化使用 countRef3 = useCurrentValue(count); 就可以隐藏组件内部的一些与业务逻辑无关的代码。

点击三次,可以发现每次都可以拿到最新的对象的值

56927-0cyffrp9bhs.png

四、useEffect 一些旁门左道的测试

1、执行顺序

上面我们可以看到,本身 useEffect 会在每次 render 的时候执行一次,并且都是在组件内部的逻辑代码之后执行。

    useEffect(() => {
        console.log('useEffect [2]')
    });
    useEffect(() => {
        countRef2.current = count;
        console.log('useEffect')
    });

    console.log('-----render-----')

上面代码中,虽然我将 console.log('-----render-----') 放在了最后面,但是最终输出的时候,还是会先输出 ---render---,在执行 effect。

当然两个顺序的 useEffect 就是按照顺序执行了。

14873-mzzvqx1lhl.png

五、代码示例