React Hooks ——useRef解读

162 阅读4分钟

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,需要使用forwardRefforwardRef)。

如果将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个位置(获取原值)
}

image.png