React 之 Refs 详解

19,427 阅读8分钟

在介绍 Refs 之前,我们先来了解两个概念:受控组件 和 不受控组件。

受控组件

在HTML中,表单元素(如 input、textarea、select)之类的表单元素通常可以自己维护state,并根据用户的输入进行更新。而在React中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过 setState()来更新。 在此,我们将 React的state作为唯一的数据源,通过渲染表单的React组件来控制用户输入过程中表单发送的操作。 这个“被React通过此种方式控制取值的表单输入元素”被成为受控组件

不受控制组件

从字面意思来理解:不被React组件控制的组件。在受控制组件中,表单数据由 React组件处理。其替代方案是不受控制组件,其中表单数据由DOM本身处理。文件输入标签就是一个典型的不受控制组件,它的值只能由用户设置,通过DOM自身提供的一些特性来获取。

受控组件和不受控组件最大的区别就是前者自身维护的状态值变化,可以配合自身的change事件,很容易进行修改或者校验用户的输入。

在React中 因为 Refs的出现使得 不受控制组件自身状态值的维护变得容易了许多,接下来我们就重点介绍一下 Refs的使用方式。

什么是Refs

Refs 是一个 获取 DOM节点或 React元素实例的工具。在 React 中 Refs 提供了一种方式,允许用户访问DOM 节点或者在render方法中创建的React元素。

在 React单项数据流中,props是父子组件交互的唯一方式。要修改一个子组件,需要通过的新的props来重新渲染。 但是在某些情况下,需要在数据流之外强制修改子组件。被修改的子组件可能是一个React组件实例,也可能是一个DOM元素。对于这两种情况,React 都通过 Refs的使用提供了具体的解决方案。

使用场景

refs 通常适合在一下场景中使用:

  1. 对DOM 元素焦点的控制、内容选择或者媒体播放;
  2. 通过对DOM元素控制,触发动画特效;
  3. 通第三方DOM库的集成。

避免使用 refs 去做任何可以通过声明式实现来完成的事情。例如,避免在Dialog、Loading、Alert等组件内部暴露 open(), show(), hide(),close()等方法,最好通过 isXX属性的方式来控制。

使用方式

关于refs的使用有两种方式: 1)通过 React.createRef() API【在React 16.3版本之后引入了】;2)在较早的版本中,我们推荐使用 回调形式的refs。

React.createRef()

class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.tRef = React.createRef();
  }
  render() {
    return (
      <div ref={ this.tRef }></div>
    )
  }
}

以上代码 创建了一个实例属性 this.tRef, 并将其 传递给 DOM元素 div。后续对该节点的引用就可以在ref的 current属性中访问。ref的值根据节点类型的不同结果也不同:

  1. 当ref属性用于普通 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
class TestComp extends React.Component {
  constructor(props) {
    super(props);
    // 创建一个 ref 来存储 DOM元素 input
    this.textInput = React.createRef();
    this.focusEvent = this.focusEvent.bind(this);
  }
  focusEvent() {
    // 直接通过原生API访问输入框获取焦点事件
    this.textInput.current.focus();
  }
  render() {
    return (
      <div>
        <input type="text" ref={this.textInput} />
        <input type="button" value="获取文本框焦点事件" onClick={this.focusEvent}/>
      </div>
    );
  }
}

  1. 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
class ParentComp extends React.Component {
  constructor(props) {
    super(props);
    // 创建ref 指向 ChildrenComp 组件实例
    this.textInput = React.createRef();
  }

  componentDidMount() {
    // 调用子组件 focusTextInput方法 触发子组件内部 文本框获取焦点事件
    this.textInput.current.focusTextInput();
  }

  render() {
    return (
      <ChildrenComp ref={ this.textInput } />
    );
  }
}

class ChildrenComp extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  focusTextInput() {
    this.inputRef.current.focus();
  }
  render(){
    return(
      <div>
        <input type='text' value='父组件通过focusTextInput()方法控制获取焦点事件' ref={ this.inputRef }/>
      </div>
    )
  }
}

  1. 不能在函数组件上使用 ref 属性,因为他们没有实例。
回调 Refs

React 也支持另外一种使用 refs的方式成为 “回调 refs”,可以帮助我们更精准的控制何时 refs被设置和解除。 这个回调函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。


class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
    // 使用'ref'的回调函数将 text输入框DOM节点的引用绑定到 React实例 this.textInput上
    this.inputRef = element => {
      this.textInput = element;
    }
    this.focus = () => {
      if (this.textInput) {
        this.textInput.focus();
      }
    }
  }
  componentDidMount() {
    this.focus();
  }
  render() {
    return (
      <div>
        <input type='text' ref={ this.inputRef } />
      </div>
    );
  }
}

React 将在组件挂载时会调用 ref 回调函数并传入DOM 元素,当卸载时调用它并传入 null。 在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。 在类组件中,通常父组件 把它的refs回调函数 通过props的形式传递给子组件,同时子组件把相同的函数作为特殊的 ref属性 传递给对应的 DOM 元素。

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

class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
    // 初始化 flag 值为 init
    this.state = {
      flag: 'init'
    }
    this.focus = () => {
      if (this.textInput) {
        this.textInput.focus();
      }
    }
  }
  componentDidMount() {
    this.focus();
    // 当执行完 render 首次渲染之后,更新状态 flag 值 为 update
    this.setState({
      flag: 'update'
    });
  }
  render() {
    return (
      <div>
      {/* 通过内联回调形式定义 ref  */}
      <input type='text' value={this.state.flag} ref={(element) => {
        console.log('element', element); // 将传入的 element 输出控制台
        this.textInput = element;
      }} />
      </div>
    )
  }
}

过时 API:String 类型的Refs

如果你目前还在使用 this.refs.textInput 这种方式访问refs,官方建议使用 回调函数 或者 createRef API的方式来替换。

如何将DOM 通过Refs 暴露给父组件

在极少数情况下,我们可能希望在父组件中引用子节点的 DOM 节点(官方不建议这样操作,因为它会打破组件的封装),用户触发焦点或者测量子DOM 节点的大小或者位置。虽然我们可以通过向子组件添加 ref的方式来解决,但这并不是一个理想的解决方案,因为我们只能获取组件实例而不是 DOM节点。并且它还在函数组件上无效。

在react 16.3 或者更高版本中,我们推荐使用 ref 转发的方式来实现以上操作。

ref 转发使得组件可以像暴露自己的 ref一样暴露子组件的 ref。

Ref forwarding is a technique for automatically passing a ref through a component to one of its children. This is typically not necessary for most components in the application. However, it can be useful for some kinds of components, especially in reusable component libraries.

Ref forwarding 是一种自动将ref 通过组件传递给其子节点的技术。下面我们通过具体的案例来演示一下效果。

const ref = React.createRef();
const BtnComp = React.forwardRef((props, ref) => {
  return (
    <div>
      <button ref={ref} className='btn'>
        { props.children }
      </button>
    </div>
  )
});

class TestComp extends React.Component {
  clickEvent() {
    if (ref && ref.current) {
      ref.current.addEventListener('click', () => {
        console.log('hello click!')
      });
    }
  }
  componentDidMount() {
    console.log('当前按钮的class为:', ref.current.className); // btn
    this.clickEvent(); // hello click!
  }
  render() {
    return (
      <div>
        <BtnComp ref={ref}>点击我</BtnComp>
      </div>
    );
  }
}

上述案例,使用的组件BtnComp 可以获取对底层 button DOM 节点的引用并在必要时对其进行操作,就像正常的HTML元素 button直接使用DOM一样。

注意事项

第二个ref参数仅在使用React.forwardRef 回调 定义组件时存在。常规函数或类组件不接收ref参数,并且在props中也不提供ref。

Ref转发不仅限于DOM组件。您也可以将refs转发给类组件实例。

高阶组件中的refs

高阶组件(HOC)是React中用于重用组件逻辑的高级技术,高阶组件是一个获取组件并返回新组件的函数。下面我们通过具体的案例来看一下refs如何在高阶组件钟正常使用。


// 记录状态值变更操作
function logProps(Comp) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }
    render() {
      const { forwardedRef, ...rest } = this.props;
      return <Comp ref={ forwardedRef } {...rest} />;
    }
  }
  return React.forwardRef((props, ref) => {
    return <LogProps { ...props } forwardedRef={ ref } />;
  });
}

// 子组件
const BtnComp = React.forwardRef((props, ref) => {
  return (
    <div>
      <button ref={ref} className='btn'>
        { props.children }
      </button>
    </div>
  )
});

// 被logProps包装后返回的新子组件
const NewBtnComp = logProps(BtnComp);


class TestComp extends React.Component {
  constructor(props) {
    super(props);
    this.btnRef = React.createRef();

    this.state = {
      value: '初始化'
    }
  }

  componentDidMount() {
    console.log('ref', this.btnRef);
    console.log('ref', this.btnRef.current.className);
    this.btnRef.current.classList.add('cancel'); // 给BtnComp中的button添加一个class
    this.btnRef.current.focus(); // focus到button元素上
    setTimeout(() => {
      this.setState({
        value: '更新'
      });
    }, 10000);
  }

  render() {
    return (
      <NewBtnComp ref={this.btnRef}>{this.state.value}</NewBtnComp>
    );
  }
}

最终的效果图如下:


注明:文章来源于公众号 react_native, 已经过作者授权转载。


本文主要讲述了 React 之 Refs的使用,如需获取更多React相关内容,请扫码关注 “铜板街科技” 微信公众号,并在后台回复“React” 或者 “react-native” 获取更多精彩内容。