细说Ref、useRef和forwardRef

2,134 阅读5分钟

前言

简单整理了一下ReactrefuseRefforwardRef(重点讲) 的使用。

useRefRef的区别

一个标准的Ref是一个对象:{current:null},用来绑定DOM元素的引用和组件的实例,在React16.8之前通常是用React.createRef来创建。
在生成Ref对象时,常用的有两种方法:在类组件中,我们往往使用React.createRef来生成Ref对象。而useRef通常用于函数组件Ref对象的生成,这也是React.createRefuseRef在使用上的一个区别。

简单来说:

  • useRef只能够函数组件中使用
  • React.createRef能够在类组件函数组件中使用

Ref作用

  • 组件内使用ref,获取dom元素
  • 在类组件中,ref作为子组件的属性,获取的是该子组件的实例

组件内使用ref,获取dom元素

// class组件
import React, { Component } from 'react';
class Child extends Component {
  divRef = React.createRef();
  componentDidMount() {
    console.log(this.divRef.current);
  }
  render() {
    return (
      <div ref={this.divRef}>dom元素</div>
    )
  }
}
export default Child;

// 函数式组件
import React, { Component, useEffect, useRef } from 'react';
const Child = () => {
  const divRef = useRef();
  useEffect(()=>{
    console.log(divRef.current);
  },[])
  return (
    <div ref={divRef}>dom元素</div>
  )
}
export default Child;

ref作为子组件的属性,获取的是该子组件

这种情况只适用于类组件,因为函数组件是没有状态的(hooks之前);

import React, { Component } from 'react';
class Child extends Component {
  test(value) {
    console.log(value);
  }
  componentDidMount() { }
  render() {
    return (<div>子组件</div>)
  }
}
class Parent extends Component {
  childRef = React.createRef();
  componentDidMount() {
    console.log(this.childRef.current.test("获取child的test"));
  }
  render() {
    return <Child ref={this.childRef} />
  }
}
export default Parent;

获取子组件中的dom的话

import React, { Component } from 'react';
class Child extends Component {
  componentDidMount() {
    console.log(this.props);
   }
  render() {
    return (<div ref={this.props.myRef}>子组件</div>)
  }
}
class Parent extends Component {
  childRef = React.createRef();
  componentDidMount() {
    console.log(this.childRef.current);// 子组件的dom
  }
  render() {
    return <Child myRef={this.childRef} />
  }
}
export default Parent;

函数组件需要用到forwardRef,接下来会讲。

forwardRef作用

它是react16新增的方法,返回react组件,两个非常有用的场景来自官网

  • 转发refs到DOM组件
  • 在高阶组件中转发refs

使用方式

import React, { useState, useEffect, useRef } from 'react';
const Child = React.forwardRef((props,ref)=>{
  return <input ref={ref}></input>
})
const Parent = ()=>{
  const childrenRef = useRef();
  useEffect(()=>{
    console.log(childrenRef.current); // child input
  },[])
  return <Child ref={childrenRef}></Child>
}
export default Parent

仔细一看,怎么和上面获取子组件中的dom 区别不大。为什么还需要用forwardRef去包装一下呢?还有为什么ref不能作为组件属性注入到props中?

<Child myRef={this.childRef} />

官网是这样描述:Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。
什么意思呢?就是ref一般是不会用到的,也让开发者不要滥用。

这是因为使用ref会脱离React的控制
比如:DOM聚焦 需要调用input.focus(),直接执行DOM API是不受React控制的。但为了保证应用的健壮,React也要尽可能防止他们失控。

失控的Ref

首先来看不失控的情况:

  • 执行ref.currentfocusblur等方法
  • 执行ref.current.scrollIntoView使element滚动到视野内
  • 执行ref.current.getBoundingClientRect测量DOM尺寸

失控的情况呢?

  • 执行ref.current.remove移除DOM
  • 执行ref.current.appendChild插入子节点

通过ref执行这些操作就属于失控的情况。看下面代码:

import React,{useState,useRef} from 'react';
export default function Counter() {
  const [show, setShow] = useState(true);
  const ref = useRef(null);
  return (
    <div>
      <button
        onClick={() => { setShow(!show) }}>
        click
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        移除dom
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}

click:是通过React控制的方式移除P节点。
移除dom:直接操作DOM移除P节点。
如果先点击click,再点击移除dom,则会报错:

image.png
这是使用使用Ref操作DOM造成的失控情况

如何限制失控

现在问题来了,既然叫失控了,那就是React没法控制的(React总不能限制开发者不能使用DOM API吧?),那如何限制失控呢?正常情况下,基于dom封装的组件是可以直接把ref指向dom:

function MyInput(props) {
  const ref = useRef(null);
  return <input ref={ref} {...props} />;
}

但是MyInput如果作为子组件,内部就不能直接获取ref

import React, { useRef } from 'react';
function MyInput(props) {
  return <input {...props} />;
}
export default () => {
  const inputRef = useRef(null);
  const handleClick = () => {
    inputRef.current.focus();
  }
  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        input聚焦
      </button>
    </>
  );
}

现在inputRef是拿不到MyInputinput的引用。

究其原因,就是上面说的为了将ref失控的范围控制在单个组件内,React默认情况下不支持跨组件传递ref

人为取消限制

如果一定要取消这个限制,可以使用forwardRef API显式传递ref。如上面例子所示。
使用forwardRefforward在这里是传递的意思)后,就能跨组件传递ref

在例子中,我们将inputRefForm跨组件传递到MyInput中,并与input产生关联。

在实践中,一些同学可能觉得forwardRef这一API有些多此一举。

但从ref失控的角度看,forwardRef的意图就很明显了:既然开发者手动调用forwardRef破除防止ref失控的限制,那他应该知道自己在做什么,也应该自己承担相应的风险。

ref其他作用

ref除了上面的作用以外,还可以用来缓存值,在某些场景下非常有用,比如:当进入页面 5s 后,输出当前最新的 num,代码如下:

import React, { useState, useEffect } from 'react';
const Test = ()=> {
  const [num, setNum] = useState(0);
  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(num)
    }, 5000);
    return () => {
      clearTimeout(timer);
    }
  }, [])
return <button onClick={() => setNum(c => c + 1)}>
    click
  </button>
}
export default Test

以上代码,实现了初始化 5s 后,输出 num。但是因为闭包问题,num始终是之前的值:0。就算多点击click,最终也是输出:0
仔细想想,有其他办法可以解决吗?目前只有用ref来缓存了。

import React, { useState, useEffect, useRef } from 'react';
const Test = () => {
  const [num, setNum] = useState(0);
  const ref = useRef(num);
  ref.current = num;
  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(ref.current)
    }, 5000);
    return () => {
      clearTimeout(timer);
    }
  }, [])
  return <button onClick={() => setNum(c => c + 1)}>
    click
  </button>
}
export default Test

总结!

不想总结了~~~。