useRef
先来看看官网如何解释:
你应该熟悉 ref 这一种访问 DOM的主要方式。如果你将 ref 对象以
<div ref={myRef} />
形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的.current
属性设置为相应的 DOM 节点。
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在.
它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
官网的解释我将其归纳为以下几点:
- 赋给DOM的
ref
属性,可以用来访问该DOM节点 - 返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数( initialValue )。
- 返回的 ref 对象在组件的整个生命周期内保持不变
- 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方
- useRef 类似于类组件的 this
关于第1点
先来看看如何利用useRef获取DOM节点
import { useRef} from "react";
import * as ReactDOM from "react-dom";
function App() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
// inputEl.current.value = 'abc';
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
GIF 2021-12-14 17-32-31.gif
当我点击右侧的按钮,左侧的输入框就聚焦了。
关于第3、4点
有如下场景:这是我写的一个AutoComplete组件,handleChange
是在正常输入的时候改变value的值而触发搜索的事件函数,handleSelect
函数是当我点击搜索结果列表的其中一项后将列表值插入到输入框中,但这个操作同样也改变了value的值从而导致触发搜索——很明显这次搜索是不必要的。这个时候我试图使用一个state来作为标识符(flag),当正常输入(执行handleChange
)的时候set其为true,当点击列表(执行handleSelect
)的时候set其为false,然后在useEffect中根据它的值来判断是否要执行搜索。这个解决方案本身没有任何问题,足矣解决我的问题。但,我们知道,一个state的改变是会触发组件渲染的,而这个标识符却没有任何触发渲染的必要,它仅仅需要起到一个标识的作用而已!
此时,就用到了useRef的第3、4点特性。
我使用useRef初始化了后给到triggerSearch
变量,在handleChange
将triggerSearch.current
设置为true,在handleSelect
中将triggerSearch.current
设置为false,然后在useEffect中使用它拦截即可。
useRef不会随组件的重新渲染而被重置,同时它的改变也不会触发组件的重新渲染。
关于第5点
直接看如下Demo:
import React, { useState, useRef } from "react";
import ReactDOM from "react-dom";
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
运行结果如下:
当我在点击
show alert
按钮后接着在Click me
按钮点击了3次,alert出的count
仍然是0。产生这个现象的原因涉及到event loop的概念:setTimeout
在事件队列中形成一个新的任务(宏任务),其回调函数是其微任务,当我点击show alert
按钮的时候setTimeout
中拿到初始渲染时声明的count
,在回调函数执行的时候也不会改变。之后点击Click me
按钮后count
被重新创建,与setTimeout
中访问的最初那个count
不再有任何关系,因此setTimeout
中的那个count
就是0。
而useRef
的第5点特性正是可用来解决这个问题。
useRef
创建的对象能够在组件中始终保留来存储某些东西(就像class组件中的this对象一样)即使组件重新渲染也不会被改变。
function Example() {
const [count, setCount] = useState(0);
const currentCount = useRef();
currentCount.current = count;
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + currentCount.current);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
GIF 2021-12-22 17-46-10.gif
代码中
currentCount
对象始终存在于组件中,即使组件发生重新渲染,它始终保留着。那么setTimeout
的回调函数拿到的始终都是同一个存储着最新count
的currentCount
。
完。