主要围绕这两个问题做一些记录:
-
useRef
和createRef
的区别? -
useRef
为什么可以用来封装成usePrevious
?
useRef 和 createRef 的区别
两者效果相同的情景
学习 useRef
的时候,冒出了一个疑问,这个和 createRef
有什么区别呢,一开始用下面的这个例子,两者的效果是相同的:
import React, { useRef } from "react";
const FocusInput = () => {
const inputElement = useRef();
// 或者 const inputElement = React.createRef(), 效果一样
const handleFocusInput = () => {
inputElement.current.focus();
};
return (
<>
<input ref={inputElement} />
<button onClick={handleFocusInput}>focus</button>
</>
);
};
export default FocusInput;
需要注意的是,上述的组件没有状态更新,相当于挂载之后就不会重新渲染。
两者效果不同的情景
但是当组件涉及到重新渲染的时候,就可以看出两者的区别了,如下所示:
import React, { useRef } from "react";
const FindDiff = () => {
const [count, setCount] = React.useState(1);
// 分别创建两个 ref
const refByUseRef = useRef();
const refByCreateRef = React.createRef();
if (!refByUseRef.current) {
refByUseRef.current = count;
}
if (!refByCreateRef.current) {
refByCreateRef.current = count;
}
return (
<>
<p>count: {count}</p>
{/* useRef 实时值 */}
<p>refByUseRef count: {refByUseRef.current}</p>
{/* createRef 实时值 */}
<p>refByCreateRef count: {refByCreateRef.current}</p>
<button onClick={() => setCount(count + 1)}>render</button>
</>
);
};
export default FindDiff;
上述代码执行的结果如图1所示:
为什么出现不同的结果
组件随着 count 的值的改变重新渲染,每次 count 改变,DOM 重新渲染之后,createRef
就会重新创建,生成一个新的引用地址,新的 ref 初始值为 null,页面渲染时被赋予当前 count 的值。
但是 useRef 一旦被创建,在组件的整个生命周期中,react 都会给它保持同一个引用,useRef
返回的 {current: xxx}
对象也将在整个生命周期中保持同一个值,除非手动改变 current
的值。
所以 refByCreateRef count
会随着 count 改变,但是 refByUseRef count
将始终保持在第一次渲染时的值
useRef 更进一步的应用
使用useRef 封装成 usePrevious
有些场景下,我们可能需要获取到当前状态的上一个状态的值,使用 useRef
可以实现这个需求。
如下所示,可以获取到上一次 count 的值:
import React, { useRef, useState, useEffect } from "react";
const GetPrevStatus = () => {
const [count, setCount] = useState(0);
const prevCount = useRef();
useEffect(() => {
prevCount.current = count;
});
return (
<>
<p>prevCount: {prevCount.current}</p>
<p>currentCount: {count}</p>
<button onClick={() => setCount(count + 1)}>click</button>
</>
);
};
export default GetPrevStatus;
图2
图3
通过 useRef 是怎么获取到上一次状态的值的呢?
首先需要明确 useEffect
中的副作用函数会在第一次挂载之后,以及每一次重新渲染之后被调用,也就是 class 组件的 componentDidMount
componentDidUpdate
这两个阶段中,这就导致 effect 函数会在浏览器完成画面渲染之后延迟调用。
在上面的代码中就是,在第一次挂载之后,也就是页面渲染成功之后,此时 prevCount.current = 0
这个副作用函数才被调用,但此时页面早已经渲染成功了,<p> 标签中的 {prevCount.current}
的值已经被渲染到页面上了,如图2所示,这个值是为 null,也就是说这一次渲染 {prevCount.current}
引用的是初始值,而不是执行 prevCount.current = 0
之后拿到的值。
同理看图3,当 count 变成 5 的时候,触发页面重新渲染,此时prevCount.current
的值是上一次渲染成功之后,执行 prevCount.current = 4
得到的值,此时引用这个值,4 被渲染到页面上,页面渲染成功之后再次延迟调用 prevCount.current = 5
, 得到新的值,供下一个引用使用。如此,便可以拿到上一次状态的值。
接着,可以将上面的代码进一步封装成一个 Hook:
const usePrevious = (state) => {
const ref = useRef();
useEffect(() => {
ref.current = state;
});
return ref.current;
};
通过 const prevCount = usePrevious(count)
我们可以拿到 count 上一次的状态值,因为 usePrevious
中的 return 是同步的,而 useEffect
是延迟执行的,所以当我们调用 usePrevious
时,return ref.current
返回的始终是上一次执行 ref.current = state
得到的值。
简言之就是:useEffect
是在每次渲染之后才会触发副作用函数的,是延迟执行的。而 return 语句是同步的,所以 return 的时候, ref.current
还是旧值。
另外需要注意:
当 useRef 的 current 发生变化时,页面不会收到通知,也就是说更改 .current 属性不会导致页面重新渲染,因为引用地址始终是同一个。
网友评论