react中的refs使用说明

458 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情

refs是什么

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

我们可以给元素添加一个ref属性,通过设置的ref访问该子元素的实例。这个元素可以是直接的html标签,也可以是自定义的class组件或函数组件。

refs的使用

1. 在class组件中使用createRef

我们使用createRef创建ref有以下几种情况:

1、直接绑定到标签元素

①使用React.createRef创建ref

②将创建的ref设置在元素上

③通过ref.current访问元素

import React, {Component, createRef} from 'react';
class RefsTest extends Component{
  constructor(props) {
    super(props);
    this.nameRef = createRef();
  }
  bthClick() {
    console.log(this.nameRef.current, this.nameRef.current.value)
  }
  render() {
    return (
    <div>
      <div>姓名:<input ref={this.nameRef} /></div>
      <button onClick={this.bthClick.bind(this)}>提交</button>
    </div>
    )
  }
}

image-20220216192959575.png

我们可以看到上面示例运行后拿到了input元素,并且可以访问该元素的方法和属性。

2、父组件class组件时,访问子元素也为class组件

①我们可以直接在class组件中访问一个子class组件的实例

②我们可以访问这个子class组件的方法/属性

③我们可以通过访问子class组件的方法获取到子class中的某些具体子元素

我们看一下这个示例:

import React, {Component, createRef} from 'react';
class RefsTest extends Component{
  constructor(props) {
    super(props);
    this.ageRef = createRef()
  }
  bthClick() {
    console.log(this.ageRef.current)  // 访问class组件
    console.log(this.ageRef.current.getInputRefValue())  //访问组件中的方法,获取到子class组件中某个元素的值
  }
  render() {
    return (<div>
      <AgeInput ref={this.ageRef} />
      <button onClick={this.bthClick.bind(this)}>提交</button>
    </div>)
  }
}

class AgeInput extends Component{
  constructor(props) {
    super(props);
    this.inputRef = createRef();
  }
  getInputRefValue() {
    return this.inputRef.current.value
  }
  render() {
    return <div>
      年龄:<input ref={this.inputRef}></input>
    </div>
  }
}

image-20220216195815040.png

我们在RefTest组件中拿到了AgeInput组件,然后通过AgeInput组件getInputRefValue方法又访问到了AgeInput组件的子元素input框的属性。通过该方法我们实现了组件跨层级通信。

2. Refs 与函数组件 (hook用法)

默认情况下,我们不能在函数组件上使用 ref 属性,因为它们没有实例。在讲hook用法前,我们先看一下class组件中的函数组件怎么使用ref:

1、父组件为class组件,访问子组件为函数组件

①我们直接用createRef创建ref

②将函数组件使用React.forwardRef处理生成新的组件

③将创建好的ref设置在生成的新的组件上

④在函数组件中接收ref绑定到实际需要访问的元素上

⑤通过ref.current访问元素

class RefsTest extends Component{
  constructor(props) {
    super(props);
    this.ageRef = createRef()
  }
  bthClick() {
    //访问函数组件
    console.log(this.ageRef.current, this.ageRef.current.value)
  }
  render() {
    const AgeWithRef = React.forwardRef(AgeInput)//这一步很重要
    return (<div>
      <AgeWithRef ref={this.ageRef} />
      <button onClick={this.bthClick.bind(this)}>提交</button>
    </div>)
  }
}
​
function AgeInput(props, ref) {
  return(
    <div>
      年龄:<input ref={ref}></input>
    </div>
  )
}

我们通过这个示例,可以发现访问class组件与函数组件的不同:

①函数组件必须使用React.forwardRef做ref转发处理,否则直接使用会报错

②函数组件接收了ref之后是直接绑在其内部需要的元素上,无法访问整个函数组件实例;而class组件是可以直接拿到整个组件实例的,需要访问class组件中的子元素,需要在子的class组件中再使用createRef生成绑定目标元素input框,自行通过组件内的放法getInputRefValue将这个实例传递到最外层RefTest组件。

2、在父组件为函数组件时:

我们在函数组件中使用ref只能指向一个DOM元素或者class组件

在函数组件中我们就需要用**hook的方法**了

①使用useRef初始化一个ref

②将这个ref设置在需要的元素上

③通过定义的ref.current访问元素

import React, {useRef} from 'react';
function RefsTest() {
  const nameRef = useRef(null)
  const btnClick = () => {
    console.log(nameRef.current, nameRef.current.value)
  }
  return(
    <div>
      姓名:
      <input ref={nameRef}></input>
      <button onClick={btnClick}>提交</button>
    </div>
  )
}

3.使用callback方法

①初始化constructor中定义ref为null

②定义一个回调函数,其回调参数就是设置的需要的元素

③在需要的元素上设置ref属性为这个回调函数

④访问该元素直接使用定义的this.xxref,不需要再.current属性才拿到元素了

class RefsTest extends Component{
  constructor(props) {
    super(props);
    // 1.初始化ref
    this.nameRef = null;
    this.state = {
      num: 0
    }
  }
// 2.定义回调函数
  setNameRef = el => {
    // callback方式可以接收一个element,这个el就是当前的input框。
    console.log('定义callback函数设置ref', el)
    this.nameRef = el
  }
  componentDidMount() {
    // React会在组件挂载时,调用ref的回调函数并传入DOM元素,当卸载时也调用回调函数并传入参数为null
    // 在componentDidMount和componentDidUpdate触发前,React会保证refs一定是最新的
    this.nameRef.focus();//实现input框获取焦点
  }
  bthClick() {
    console.log(this.nameRef, this.nameRef.value)// 没有.current
  }
​
  changeState = () => {
    console.log('state--组件重新渲染')
    this.setState({
      num: this.state.num+1
    })
  }
  
  render() {
    return (<div>
      {/* 3.元素上设置回调函数 */}
      <div>
        姓名:
        <input ref={this.setNameRef} />
      </div>
      <button onClick={this.bthClick.bind(this)}>提交</button>
      <button onClick={this.changeState}>更新组件{this.state.num}</button>
    </div>)
  }
}

绑定ref到input上的回调函数有直接行内写法:

<input ref={el=>{
      console.log('行内设置ref', el)
      this.nameRef = el
}} />

这种行内写法和定义callback方法有什么区别呢?

①当我们点击更新组件的按钮时,通过setState触发组件重新渲染可以看到只有这个更新组件这个按钮事件的输出

image-20220216212206401.png

②当我们把input框的ref改成行内写法后再触发组件重新渲染时看到输出:

image-20220216212655223.png

看出来每次重新渲染时都会执行这个ref组件的卸载再挂载。其原因在于行内设置属性为函数,每次重新渲染时相当于重写定义了一次ref属性的这个函数,造成了React进行diff时判断时其属性是不一样的,所以无法组件复用,只能把这个节点删除再新增,所以就会出现上面的打印结果。

所以在使用时我们应该注意避免行内写法,大部分情况下我们的callback中并没有太多的逻辑操作,基本上只是ref的赋值,所以它用行内写法也是无关紧要的。但是为了性能优化考虑,我们还是需要主要避免。

使用refs需要注意的几点

  1. 无法拿到整个函数组件,因为函数组件没有实例。只能使用forwardRef将其转发到函数组件内的某些元素上
  2. React之前版本的String 类型的 Refs在后续会被弃用,应当避免通过String方法使用ref了
  3. 使用callback方式使用ref时注意避免使用行内写法