React ref的使用

2,663 阅读9分钟

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

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

何时使用Refs

在某些特定的场景,必须使用ref来访问DOM元素。

  • input的focus、文本select、媒体播放器。
  • 触发强制动画。
  • 集成第三方 DOM 库。

避免使用 refs 来做任何可以通过声明式实现来完成的事情。

举个例子,避免在 Dialog 组件里暴露 open() 和 close() 方法,最好传递 isOpen 属性。

React提倡不要过度使用ref,除非遇到input的focus类似必须DOM的操作的场景,我们才会使用ref,否则应该使用state和props解决问题,脱离复杂的DOM操作。

使用ref的三大原则:

  1. 可以在dom元素上面使用ref属性,ref表示对DOM元素节点引用。
  2. 可以在class组件上面使用ref属性,ref表示对React组件的实例引用。
  3. 在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件:
  4. 不能在函数组件上面使用ref属性,原因:函数组件没有实例,父组件获取子组件的ref,其实是在获取子组件的实例;

React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例;需要区分一下,ReactDOM.render()渲染组件时返回的是组件实例;而渲染dom元素时,返回是具体的dom节点。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div>Hello Component</div>
    );
  }
}

const renderComponent = ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
);

console.log('render组件:', renderComponent)

const renderDom= ReactDOM.render(
  <div>Hello Dom</div>,
  document.getElementById('root')
);

console.log('render的dom节点:', renderDom)

输出结果为:

render组件: MyComponent {props: {…}, context: {…}, refs: {…}, updater: {…}, textInput: {…}, …}
render的dom节点: <div>​Hello Dom​</div>​

创建和获取ref

在reactd的版本历史上,出现了三种创建ref的方式:string ref,callback ref,React.createRef。无论哪种方式,都是为ref属性赋值,ref和key一样,都是关键字,为React内部使用。 另外,值得注意的是,所有的ref获取,最好在组件加载结束之后,否则无法获取值。因为组件加载后,dom才准备好了是吧。

string ref 使用

class MyComponent extends React.Component {
  componentDidMount() {
    this.refs.myRef.focus();
  }
  render() {
    return <input ref="myRef" />;
  }
}

callback ref 使用

class MyComponent extends React.Component {
  componentDidMount() {
    this.myRef.focus();
  }
  render() {
    return <input ref={(ele) => { this.myRef = ele; }} />;
  }
}

React 支持给任意组件添加特殊属性。ref属性接受一个回调函数,它在组件被加载或卸载时会立即执行。

  • 当给 HTML 元素添加 ref 属性时,ref 回调接收了底层的 DOM 元素作为参数。
  • 当给组件添加 ref 属性时,ref 回调接收当前组件实例作为参数。
  • 当组件卸载的时候,会传入null ref 回调会在componentDidMount 或 componentDidUpdate 这些生命周期回调之前执行。

关于回调 refs 的说明

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。

在组件发生了更新时,ref都会重新创建。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的

class Test extends React.Component {
  componentDidMount(){
  // 获取
    console.log(this.second);
  // <input value="second">
  }
  createRef = (dom) => {
  	this.second = dom;
  }
  render() {
  // 创建
    return <input value="second" ref={this.createRef} />
  }
}

React.createRef 使用

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Child extends React.Component{
  render(){
    return <div>This is child</div>
  }
}
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  componentDidMount(){
    console.log(this.myRef.current)
  }
  render() {
    return (
      <div><Child ref={this.myRef}/></div>
    );
  }
}

ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
);

React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例。

此处示例是react16.3+版本的使用(如果想使用旧版本请看下文),步骤有三个:

  1. createRef方法 生成ref 对象
  2. render的时候 接收 子组件或者dom元素的ref属性
  3. 用this.inputRef.current 来获取这个 子组件或者dom元素

将 DOM Refs 暴露给父组件

在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。

虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。

如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档中有一个详细的例子。

Refs 转发

注意:ref不能通过props传递,那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。最常见的案例如下所述。

React.forwardRef

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  componentDidMount() {
    console.log(this.textInput.current)
  }

  render() {
    return (
      <FancyButton ref={this.textInput}>Click me!</FancyButton>
    );
  }
}

ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
);

使用FancyButton的组件可以获取底层DOM节点button的ref,并在必要时访问,就像其直接使用 DOM button 一样。

以下是对上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给<FancyButton ref={ref}>
  3. React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

注意
第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

在高阶组件中转发refs

这个技巧对高阶组件(也被称为 HOC)特别有用。让我们从一个输出组件 props 到控制台的 HOC 示例开始:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}
const LogPropsHoc = logProps(FancyButton)

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  componentDidMount() {
    console.log(this.textInput.current)
  }

  render() {
    
    return (
      <LogPropsHoc ref={this.textInput} label='Click Me'>Click me!</LogPropsHoc>
    );
  }
}

ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
);

“logProps” HOC 透传(pass through)所有 props 到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 “fancy button” 组件的 props:

上面的示例有一点需要注意:refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

这意味着用于我们 FancyButton 组件的 refs 实际上将被挂载到 LogProps 组件:

LogProps {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternalFiber: FiberNode, …}
    context: {}
    props: {label: "Click Me", children: "Click me!"}
    refs: {}
    state: null
    updater: {isMounted: ƒ, enqueueSetState: ƒ, enqueueReplaceState: ƒ, enqueueForceUpdate: ƒ}
    _reactInternalFiber: FiberNode {tag: 1, key: null, stateNode: LogProps, elementType: ƒ, type: ƒ, …}
    _reactInternalInstance: {_processChildContext: ƒ}
    isMounted: (...)
    replaceState: (...)
    __proto__: Component

幸运的是,我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件。React.forwardRef 接受一个渲染函数,其接收 props 和 ref 参数并返回一个 React 节点。例如:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }
  
  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });;
}
const LogPropsHoc = logProps(FancyButton)

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  componentDidMount() {
    console.log(this.textInput.current)
  }

  render() {
    
    return (
      <LogPropsHoc ref={this.textInput} label='Click Me'>Click me!</LogPropsHoc>
    );
  }
}

ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
);

注意 React.forwardRef 回调的第二个参数 “ref”。我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”,然后它就可以被挂载到被 LogProps 包裹的子组件上。

<button class="FancyButton">Click me!</button>

Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref,Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

总结

React提供ref的初衷是给开发者一个可获取实际DOM的工具。 你可能首先会想到使用 refs 在你的 app 中“让事情发生”。如果是这种情况,请花一点时间,认真再考虑一下 state 属性应该被安排在哪个组件层中。通常你会想明白,让更高的组件层级拥有这个 state,是更恰当的。查看 状态提升 以获取更多有关示例。

参考文章

官网refs使用::zh-hans.reactjs.org/docs/refs-a…

官网refs转发:zh-hans.reactjs.org/docs/forwar…

链接:juejin.cn/post/684490…

链接:juejin.cn/post/684490…

链接:www.cnblogs.com/cangqinglan…