useRef
简介
React官方文档对useRef的解释如下:
useRef是一个React Hook,它能帮助引用一个不需要渲染的值。
const ref = useRef(initialValue)
定义
useRef的全称是use Reference,就是用来引用一个可变的对象,这个对象能够在组件的整个生命周期中保持不变,并且能用来访问DOM元素、存储可变的值和保存定时器ID、事件监听器等。
参数: initialValue
ref对象的current属性的初始值,表示为:{current : initialValue}。
它可以是任意类型的值。
这个值在首次渲染后被忽略。
React 内部为每个组件维护一个 Fiber 节点,其中存储了 Hook 链表。useRef 创建的 ref 对象在首次渲染时初始化并存储在 Hook 对象中,后续重新渲染时直接返回同一个对象,因此初始值被忽略。DOM 挂载时,React 将实际 DOM 元素赋值给 ref.current,这个引用在整个组件生命周期中保持不变。
返回值
useRef返回一个只有一个属性的对象即{current:xxx},其中xxx与初始值或后续行为有关,我们不仅能够将ref挂载到普通HTML标签上也能够挂载到类与函数组件上。
当ref挂载到普通HTML标签时,ref直接指向真实的DOM元素,挂载到类组件时,ref可以直接接收ref,会指向组件实例。而函数组件默认不能直接接收ref,需要使用forwardRef(forwardRef)。
如果将ref对象作为一个JSX节点的ref属性传递给React的话,React可以为它设置current属性,并且后续的渲染中,useRef将返回同一个对象,意味着这个对象在整个生命周期中保持不变。(见问题解释——为什么这个对象能在整个生命周期中保持不变?)。
问题解释
1. 可变的对象是什么?
可变的对象是指{current : xxx},因为current的属性不像useState修改时会触发重新渲染,而ref.current并不会触发组件的重新渲染(见问题3解释——为什么useRef变化不会主动使页面渲染而useState可以?)。ref.current在组件重新渲染之间保持不变,因为ref对象本身在组件的整个生命周期中都是同一个对象(见问题2解释——为什么这个对象能在整个生命周期中保持不变?),这个对象可以存储任何值,包括DOM 元素、函数、对象、基本类型等。
2. 为什么这个对象能在整个生命周期中保持不变?
React 内部为每个组件维护一个 Fiber 节点,其中包含 Hook 链表。useRef 创建的 ref 对象存储在 Hook 对象的 memoizedState中,组件重新渲染时 React 会复用同一个 Hook 对象,不会创建新的对象,地址保持不变,因此 ref 对象引用保持不变
3. 为什么useRef变化不会主动使页面渲染而useState可以?
React 设计上只监听 state 和 props 的变化来触发重新渲染。useState 返回的 state 在 React 的响应式系统中,值变化时会触发组件重新渲染。而 useRef 返回的 ref 对象不在响应式系统中,其 current 属性变化不会触发重新渲染,即使该值可以显示在页面上。
注意事项
useRef与其他Hooks一样,必须只能在组件的顶层调用,组件的顶层是指在函数组件的函数体内部,而非任何条件语句、循环、嵌套函数或其他代码块之外的位置。
在顶层调用是为了确保Hooks的调用顺序要完全相同。
function MyComponent({ condition }) {
const [count, setCount] = useState(0); // 第1个 Hook
if (condition) {
const [name, setName] = useState(''); // 第2个 Hook - 只在 condition 为 true 时调用
}
const ref = useRef(null); // 第3个 Hook - 或者第2个 Hook(当 condition 为 false 时)
// 这样会导致调用顺序不一致!
}
内部机制: 因为React使用一个内部的"Hook链表"来追踪每个Hook的状态。
遵守Hooks的调用顺序,如果调用顺序不一致会导致状态混乱、组件崩溃和不可预测行为。
// 第一次渲染
function MyComponent() {
const [count, setCount] = useState(0); // 访问链表第1个位置
const [name, setName] = useState(''); // 访问链表第2个位置
const ref = useRef(null); // 访问链表第3个位置
const [isVisible, setIsVisible] = useState(true); // 访问链表第4个位置
}
// 第二次渲染(假设 count 更新了)
function MyComponent() {
const [count, setCount] = useState(0); // 访问链表第1个位置(获取更新后的值)
const [name, setName] = useState(''); // 访问链表第2个位置(获取原值)
const ref = useRef(null); // 访问链表第3个位置(获取原值)
const [isVisible, setIsVisible] = useState(true); // 访问链表第4个位置(获取原值)
}