React中的ref总结

1,594 阅读6分钟

用处

操作组件、子组件、元素的属性

在典型的React自上而下的数据流中, props是父组件和子组件交互的唯一方式,要修改一个子组件,你需要使用新的props来重新渲染它。但是在某些情况下,需要在典型的数据流之外强制修改子组件、获取组件的属性。被修改的子组件可能是一个React组件的实例,也可能是一个原生HTML元素。

常用场景举例:

  1. 如何在组件中获取原生HTML元素属性、方法?
  2. 如何在父组件中获取子组件的属性、方法?
  3. 如何在父组件中直接获取子组件中的某原生HTML元素?

Ref的四种创建方式

React.createRef()

React的16.3及以后的版本中,可以在实例的构造函数中使用React.createRef()方法来创建ref, 并将其赋值给实例对象的自定义属性,以便于在整个组件中都可以使用, 然后再将其附加给原生HTML元素或Class类组件的ref属性上。

 class MyComponent extends React.Component {
   constructor(props) {
     super(props);
     this.myRef = React.createRef();
   }
   render() {
     return <div ref={this.myRef} />;
   }
 }
image-20200404174317196

回调函数方式

React 也支持另一种设置 ref的方式,称为“回调 ref”。它能助你更精细地控制何时 refs 被设置和解除。

不同于通过createRef()创建的ref属性,“回调 refs”传递一个函数,并接受组件的实例对象或原生HTML DOM对象作为参数,并将该参数赋值给组件实例对象的自定义属性,以便于在整个组件中都可以使用。

 class MyComponent extends React.Component {
   constructor(props) {
     super(props);
     this.setMyRef = null;
   }
   render() {
     return < div ref={ (el) => this.setMyRef = el } />;
   }
 }

React在组件挂载时会调用refs回调函数,同时传入参数对象,在组件卸载时再次调用回调函数并传入null作为参数,在componentDidMountcomponentDidUpdate触发前更新;

通过回调函数的方式, 能够直接在父组件中操作子组件中某元素的属性, 下面回详细提到:

字符串方式 (已过时、不推荐)

直接设置ref属性为一个字符串, 通过this.refs获取

 const element = <div ref="myRef">string ref</div>
 const node = this.refs.myRef

string 类型的 refs 用法已过时,且存在一些问题

  1. 需要React去保持跟踪当前渲染的组件,有点拖累性能
  2. 当使用render callback的渲染属性(render prop)模式渲染组件内容时, 为子组件设置ref 在父组件中获取不到, 容易造成误解。
 class MyComponent extends Component {
   renderRow = () => {
     // 字符串设置的ref会被绑定到子组件DataTable上而不是父组件MyComponent
     return <input ref="input" />;
   }
   render() {
     return <DataTable renderRow={this.renderRow} />
   }
 }
 //打印发现虽然在父组件中设置的ref, 但却不是绑定在父组件中,而是绑定在子组件中的refs中, 
 //如果通过回调函数设置,就可以避免该问题, 

React.useRef()

React的16.8及以后的版本中,React推出了Hooks, 自此,我们可以通过Hooks的方式为React元素设置ref属性, 由于Hooks只能用于函数组件,所以useRef()也只能为函数组件中的元素设置ref.

 function FocusInput() {
   const inputEl = useRef();
   //inputEl.current.focus();
   return (
     <>
       <input ref={inputEl} type="text" />
     </>
   );
 }

createRef()与useRef()区别

 //useRef() 
 const inputEl = useRef();
 <input ref={inputEl} type="text" />

 //createRef()
 const inputEl = createRef();
 <input ref={inputEl} type="text" />

在使用上感觉没啥区别啊, 都是调用React的内部方法,然后都是将返回值赋值给组件内部某元素的ref属性,而且二者好像都能用在函数组件中。那React为啥还要新出一个创建refAPI呢?

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Ref 分配给实例属性,以便可以在整个组件中引用它们。

这是官网对二者的解释, 是不是也没啥不同的地方,我们通过一个例子来体现二者的区别:在一个函数组件中同时通过这两种方式创建ref

 function MyComponent() {
   console.log("------------------------render--------------------------")
   const [count, setCount] = useState(0);
   const createRef = React.createRef();
   const useRef = React.useRef();
   console.log("createRef", createRef, "useRef",useRef)
   if (!createRef.current) {
     createRef.current = count;
   }
   if (!useRef.current) {
     useRef.current = count;
   }
   console.log("createRef", createRef, "useRef",useRef)
   return (
     <div>
       <h3>count: {count}</h3>
       <button onClick={() => setCount(count + 1)}>add button</button>
     </div>
   );
 }

在上面的函数组件中,每次的状态变更都会导致函数内的代码重新执行;同时我们也知道附加在元素身上的ref属性,最终会通过其.current属性体现出来,那么在每次count状态变更时,打印的值会有什么不同吗?

image-20210731143720661

在组件每次重新渲染时,createRef生成的都是一个全新的对象,那么也就不会保存上一次的current属性值; 而useRef自始自终生成的都是同一个对象,或者说自始自终操作的的都是指向同一个对象的内存地址值;

ref转发

回到开篇时提出的三个问题,前两个都场景可以直接通过设置ref获取对应元素或组件的属性;那么第三个问题,如何在父组件中直接获取子组件中的原生HTML元素呢?

不同于组件普通的props属性, 在父组件中设置的ref不会透传下去,而是像key一样,被React接收后进行了特殊的处理, 不会出现在子组件的props对象中:对子组件设置的ref只会附加在当前子组件上, 不会向下传递, 此时通过ref对象的.current属性获取的是该子组件的实例对象。目前有两种方式:

  1. 回调ref

     function Child(props) {
       return (
         <input ref={props.childRef} />
       );
     }
     ​
     class MyComponent extends Component {
       handleClick() {
         this.childRef.
         console.dir(this.childRef)
       }
       render () {
         return (
           <div>
             <Child childRef={el => this.childRef = el }></Child>
             <p onClick={() => this.handleClick()}>MyComponent</p>
           </div>
         );
       }
     }
     ​
    

    以上面代码为例: 在父组件MyComponent中给子组件Child设置一个普通的可以往下传递的childRef属性, 那么在子组件内部就可以通过props.childRef获取该属性所对应的值;所以子组件内部的input元素应该是这个样子<input ref={el => this.childRef = el} /> ,而且这里的this代表父组件,当子组件Child被挂载时调用这个回调函数,并将input元素的DOM对象通过参数的形式传入该回调函数,这样的话在父组件中就能够直接操作子组件内部某HTML元素的属性了。非常巧妙地运用函数闭包使二者之间产生联系。 通过字符串的方式就没办法做到了,所以这也是不推荐字符串方式设置ref的原因之一。

  2. ref转发

 const Child = React.forwardRef((props, ref) => {
   return <input ref={ref} defaultValue={props.defaultValue} />
 } )
 
 class MyComponent extends React.Component{
  constructor(props) {
   super(props)
   this.childRef = React.createRef()
  }
  
  focus() {
   this.childRef.current.focus()
  }
  render () {
     return (
       <div>
         <Child defaultValue={'ref'} ref={this.childRef}></Child>
         <button onClick={() => this.focus()}>focus</button>
       </div>
     );
   }
 }

通过React.forwardRef()API对ref属性进行转发,将ref属性自动的通过组件传递到其子元素上的一种技巧;React.forwardRef()接收一个函数作为参数,该函数的第二个参数接收组件的ref属性,并能够将其向下传递;