useRef深层剖析

useRef

ref 使用场景

通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:

  • 存储 timeout ID
  • 存储和操作 DOM 元素,涉及非受控组件。赋值给标签,目的是获取 DOM 元素;赋值给类组件,目的是获取组件的实例;
  • 存储不需要被用来计算 JSX 的其他对象。

如果你的组件需要存储一些值,但不影响渲染逻辑,请选择 ref。

类组件的三种创建和使用方式

方式一——字符串形式的 ref(过时了,未来版本可能移除,开发时不推荐使用,效率低

通过 this.refs访问

1
<input ref="input1"/>
方式二——回调形式的 ref
1
React帮我们调用回调函数,并且将当前的DOM节点传入回调函数
1
2
3
4
5
6
<input
ref={(currentNode) => {
this.input1 = currentNode;
}}
/>
// currentNode是当前所处的节点,也就是这个input

不能通过 this.refs访问,因为这些 ref 都成了实例属性

React 不会帮你执行未知属性的回调函数

1
2
3
4
5
6
7
8
<input
ref={(c) => (this.input1 = c)}
ahh={() => {
console.log(1);
}}
type="text"
placeholder="点击按钮提示数据"
/>

如果 ref 回调函数是以**内联函数的形式定义的,在更新过程中它会被执行两次,第一次传入参数 null,第二次传入参数 DOM 元素。这是因为在每次渲染时创建一个新的实例,所以 React 清空旧的 ref 并设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无效的(无关紧要)**

方式三——createRef 创建 ref 容器(最推荐

React.createRef 调用后可以返回一个容器,该容器可以存储被 ref 所标识的节点,该容器是“专人专用”的,多个 ref 的值可以相互独立

ref 是实例的属性,不能通过 this.refs 访问

this.myRef.current 是当前引用的 DOM,this.myRef.current.value 是当前 DOM 的值

1
2
myRef = React.createRef();
<input ref={this.myRef} />

hooks 组件通过 useRef 创建 ref 对象

在函数组件中,可以基于 useRef获取 DOM 元素!类似于类组件中的 :

  • ref={x=>thix.box=x}
  • React.createRef

函数组件中创建 ref 对象的两种方法:

  • let box1 = useRef(null)
  • let box2 = React.createRef();

注意:

React.createRef 也是 ref 对象,在类组件和函数组件中都可以使用

useRef 只能在函数组件中使用,所有的 hooks 函数都只能在函数组件中使用,在类组件中使用会报错

ref 只能在 DOM 创建之后才能获取 DOM 元素,也就是说在 useLayoutEffect 阶段就可以使用

createRef 性能比 useRef 差——每次渲染创建新 ref 对象

  • createRef 每次渲染都会返回一个新的引用
  • 而 useRef 每次都会返回相同的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { useState, useEffect, useRef } from "react";
import { Button } from 'antd';
import './Demo.less';

let prev1,
   prev2;
const Demo = function Demo() {
   let [num, setNum] = useState(0);
// 函数组件中创建ref对象的两种方法
   let box1 = useRef(null),
       box2 = React.createRef();
   if (!prev1) {
       // 第一次DEMO执行,把第一次创建的REF对象赋值给变量
       prev1 = box1;
       prev2 = box2;
  } else {
       // 第二次DEMO执行,我们验证一下,新创建的REF对象,和之前第一次创建的REF对象,是否一致?
       console.log(prev1 === box1); //true useRef再每一次组件更新的时候(函数重新执行),再次执行useRef方法的时候,不会创建新的REF对象了,获取到的还是第一次创建的那个REF对象!!
       console.log(prev2 === box2); //false createRef在每一次组件更新的时候,都会创建一个全新的REF对象出来,比较浪费性能!!
       // 总结:在类组件中,创建REF对象,我们基于 React.createRef 处理;但是在函数组件中,为了保证性能,我们应该使用专属的 useRef 处理!!
  }

   useEffect(() => {
       console.log(box1.current);
       console.log(box2.current);
  });

   return <div className="demo">
       <span className="num" ref={box1}>{num}</span>
       <span className="num" ref={box2}>哈哈哈</span>
       <Button type="primary" size="small"
           onClick={() => {
               setNum(num + 1);
          }}>
          新增
       </Button>
   </div>;
};

总结:在类组件中,创建 Ref 对象,基于 React.createRef 处理;在函数组件中为了保证性能使用 useRef

ref 的 DOM 用法总结【useRef】

  • 给元素标签设置 ref,目的:获取对应的 DOM 元素
  • 给类组件设置 ref,目的:获取当前调用组件创建的实例(后续可以根据实例获取子组件中的相关信息)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 基于ref获取子组件的实例,这样基于实例,可以调用子组件内部,挂载到实例上的东西
    class Child extends React.Component {
       state = { x: 1000 };
       render() {
           return <div className="child-box">
              {this.state.x}
           </div>;
      }
    }
  • 给函数组件/hooks 设置 ref,直接报错:Function components cannot be given refs. Attempts to access this ref wil fail,但是可以配合 React.forwardRef 实现 ref 的转发。目的:获取函数子组件内的某个 DOM 元素
    1
    2
    3
    4
    5
    6
    7
    // 基于forwardRef实现ref转发,目的:获取子组件内部的某个元素
    const Child = React.forwardRef(function Child(props, ref) {
       // console.log(ref); //在DEMO中,调用Child的时候,传递的ref对象「x」
       return <div className="child-box">
           <span ref={ref}>哈哈哈</span>
       </div>;
    });

useRef深层剖析
https://hugtyftg.github.io/2023/06/19/useRef/
作者
mmy@hugtyftg
发布于
2023年6月19日
许可协议