React Hook useEffect & useRef & 自定义 Hook 实现渲染闭包访问最新值
本文是在: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);
}
如果点击三次,输出结果是:
每次渲染比包中,我们拿到的 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
:
不过我们可以发现,上面这种写法会导致我们每次都要显示的写一行代码 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:
- 第 1 次点击
第一次点击之后,还是会首先执行一次 render 字符串的输出,然后执行 useEffect
不同的地方在于在方法中 console.log(currentRef.count)
的时候,拿到的已经是最新的值,这就说明,uesEffect 已经在 render 操作完成执行。
- 快速点击3次
继续快速点击 3 次,首先每次都会执行 render 字符串输出以及 effect 的执行,并不会等待 setTImeout 相关的执行,在 setTimeout 中输出的时候,因为已经执行了 useEffect ,对象已经更新,因此我们是可以拿到最新的 state
三、自定义 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);
就可以隐藏组件内部的一些与业务逻辑无关的代码。
点击三次,可以发现每次都可以拿到最新的对象的值
四、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 就是按照顺序执行了。
五、代码示例
文章版权:Postbird-There I am , in the world more exciting!
本文链接:http://www.ptbird.cn/react-hook-useEffect-useRef-current-data.html
转载请注明文章原始出处 !