关于 React Refs 那些事

Refs 一个专业术语

提供了一个可访问 DOMReact 元素 的方式

简单点说,提供了可手动获取并直接操作 DOM 的方式

比如 input 元素聚焦

文件上传功能等

而干活的是 ref 属性

# 名词解释

  • ClassComponent 类组件
  • FunctionComponent 函数组件
  • React Hook 钩子
  • callback ref 回调 ref

# 属性值类型根据 ref 绑定的节点类型而不同

  1. ref 绑定节点类型为 DOM 元素 ref 表示这个具体 DOM 元素
  2. ref 绑定节点类型为 ClassComponent ref 表示这个类组件(实例)
  3. ref 绑定节点类型为 FunctionComponent,注意不能直接绑定
    • 因为它不是 DOM 或 ClassComponent
    • 本身 props 上默认不存在 ref,通过 React.forwardRef 函数转发时存在,其内部要绑定 DOM 或 ClassComponent 才可以(本质绑定 DOM 或 ClassComponent)

# 创建 ref 方式

callback ref

无论哪种方式创建 ref 引用

都包含 current 属性,包含 ref 具体值

  1. React.createRef 方式
    • 用于 ClassComponent,不建议用于 FunctionComponent,在 FunctionComponent 重新渲染被初始化创建新的 ref 引用(详情参考下面的差异对比)
    • 不可以传参
    • current 属性在 Typescript 中提示为 只读
    • createRef<T> 泛型函数无默认值,被设置为 unknow
  2. React.useRef 方式
    • React Hook 一种,遵循 Hook (opens new window) 规则,用于 FunctionComponent 或其他 Hook, 处于函数作用域顶层被调用
    • 可用于数据存储或性能优化,数据保存到 current 属性上
    • 可传参,参数类型任意
    • 创建一次,更新渲染不创建只复用
    • useRef<T = undefined> 泛型函数默认值为 undefined
    • current 属性值变化不可被监测到,同时不触发组件render 视图更新
  3. callback ref (opens new window) 方式
    • 解决 ref 值变化无法被监测
    • ClassComponent 内联绑定或 FunctionComponent 绑定时,再次更新时被调用两次,第一次打印值为 null,第二次为具体绑定的 node 对象,原因每次渲染时会创建一个新的函数组件实例(此时 callback ref 也重新初始化),React 清空旧的 ref 并且设置新的
    • 在 ClassComponent 中,
      • callback ref 绑定内联函数上时,初始化 render 调用一次,再次 render 调用两次
        export default class Child extends React.PureComponent {
          state = {
            a: 100
          }
          constructor(props: any) {
            super(props)
          }
      
          onCount() {
            this.setState({
              a: Math.random()
            })
          }
      
          render() {
            return (
              <div>
                <p ref={(node: any)=> {
                  // 初始化打印一次 结果:node 对象
                  // 下次渲染更新,打印两次 结果:第一次为 null, 第二次为 node 对象
                  console.log(node)
                }}>{this.state.a}</p>
                <button className="btn btn-primary" onClick={this.onCount.bind(this)}>click</button>
              </div>
            )
          }
        }
      
      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
      • callback ref 绑定实例方法,初始化 render 调用一次,再次 render 不调用
      export default class Child extends React.PureComponent {
        state = {
          a: 100
        }
        cbRef: (node: any) => void
        constructor(props: any) {
          super(props)
          this.cbRef = (node: any)=> {
            // 初始化与再渲染更新,仅打印一次
            // 结果:node 对象
            console.log(node)
          }
        }
      
        onCount() {
          this.setState({
            a: Math.random()
          })
        }
      
        render() {
          return (
            <div>
              <p ref={this.cbRef}>{this.state.a}</p>
              <button className="btn btn-primary" onClick={this.onCount.bind(this)}>click</button>
            </div>
          )
        }
      }
      
      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
    • 在 FunctionComponent 中,callback ref 无论是否内联绑函数,初始化打印一次,再次渲染更新,打印两次
        function User(_props: any) {
          let [data] = useState({
            foo: 1
          })
          const [count, setCount] = useState(0)
          const renderRef = (node: any)=> {
            // 初始化打印一次 结果:node 对象
            // 下次渲染更新,打印两次 结果:第一次为 null, 第二次为 node 对象
            console.log(node)
          }
      
          return (
            <>
              <h3>{data.foo}</h3>
              <p ref={renderRef}>count: {count}</p>
              <button className="btn btn-primary" onClick={() => setCount(count + 1)}>click me</button>
            </>
          )
        }
      
        export default User
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21

# ref 转发之 forwardRef

FunctionComponentref 属性

父组件无法直接通过 ref 引用 FunctionComponent 组件获取 DOM 元素

利用 forwardRef (opens new window) 转发特性

转发后的 FunctionComponent 组件具有 ref 属性

将父组件 ref 绑定到 DOM 元素上

在父组件中可获取到子组件的具体 DOM 元素进行操作

forwardRef 操作

function InputChild(props: any) {
  return (
    <input  type="text" ref={props.inputRef} className="form-control" placeholder="说点什么" />
  )
}

const ForwardInput = forwardRef((props, ref: any) =>  <InputChild inputRef={ref} />)

function User(_props: any) {
  const ref = useRef<HTMLInputElement>()
  return (
    <>
      {/* 父组件通过 InputChild 转发返回后的组件设置 ref 属性 */}
      {/* 获取 InputChild 组件中 input 元素 */}
      {/* 父组件点击按钮设置 input 元素焦点 */}
      <ForwardInput ref={ref} />
      <button className="btn btn-primary" onClick={() => ref?.current?.focus()}>input focus</button>
    </>
  )
}

export default User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# FunctionComponent 中 createRefuseRef 差异对比

createRef 与 useRef 差异对比

如上图:

createRef 每次渲染新建,所以始终为 null

useRef 只创建一次,再渲染时复用,所以引用同一个 p 元素

FunctionComponent 推荐使用 useRef

源码录下:

function User(_props: any) {
  const ref = useRef(null)
  const ref2 = createRef<HTMLParagraphElement>()
  const [count, setCount] = useState(0)

  useEffect(() => {

  }, [count])
  console.log(count)
  console.log('useRef:', ref, 'createRef:', ref2)
  return (
    <>
      <h3>{count}</h3>
      <p ref={ref}>useRef p</p>
      <p ref={ref2}>createRef p</p>
      <button className="btn btn-primary" onClick={() => setCount(count + 1)}>click count</button>
    </>
  )
}

export default User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

扫一扫,微信中打开

微信二维码