React 重温之 Refs

什么是Refs

咱们在平常写React代码的时候,通常状况是用不到Refs这个东西,由于咱们并不直接操做底层DOM元素,而是在render函数里去编写咱们的页面结构,由React来组织DOM元素的更新。node

凡事总有例外,总会有一些很奇葩的时候咱们须要直接去操做页面的真实DOM,这就要求咱们有直接访问真实DOM的能力,而Refs就是为咱们提供了这样的能力。数组

看这个名字也知道,Refs实际上是提供了一个对真实DOM(组件)的引用,咱们能够经过这个引用直接去操做DOM(组件)app

为何会用到这个

上面有提到,咱们通常状况下是不须要用到这个东西,那具体何时才会用到呢? 看官方建议:ide

- Managing focus, text selection, or media playback.
 - Triggering imperative animations.
 - Integrating with third-party DOM libraries.

简单的来讲就是处理DOM元素的focus,文本的选择或者媒体的播放等,以及处罚强制动画或者同第三方DOM库集成的时候。函数

也就是React没法控制局面的时候,就须要直接操做Refs了。动画

怎么用

React V16版本以前,

咱们通常都是经过一个回调函数的方式,把当前组件的DOM绑定到一个实例变量上,像下面这样:this

class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
  }

  componentDidMount() {
    this.textInput.focusTextInput();
  }

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

在上面的代码中,咱们先声明一个值为null的textInput变量,而后在ref中以回调的方式将组件DOM赋值给textInput。而后就能够经过 this.textInput.focus()这样的性质来直接调用CustomTextInput这个组件的实例方法。rest

可是这个方式有如下两个不太好:code

  1. 每次组件从新渲染的时候,行内函数都会执行两次,第一次的ele的值为空,第二次才为真正的DOM对象。

    由于在每次渲染中React都会建立一个新的函数实例。所以,React 须要清理旧的 ref 而且设置新的。
    经过将 ref 的回调函数定义成类的绑定函数的方式能够避免上述问题,component

  2. 若是咱们想要将一个子组件的ref传递给父组件,可能会有点麻烦,虽然经过一个特殊的prop属性能够作到,可是感受有点不太正规。。。

React V16 版本后

React V16版本新增一个API:React.createRef(); 经过这个API,咱们能够先建立一个ref变量,而后再将这个变量赋值给组件声明中ref属性就行了。

具体看代码:

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // create a ref to store the textInput DOM element
    this.textInput = React.createRef();
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // Explicitly focus the text input using the raw DOM API
    // Note: we're accessing "current" to get the DOM node
    this.textInput.current.focus();
  }

  render() {
    // tell React that we want to associate the <input> ref
    // with the `textInput` that we created in the constructor
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} />

        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

在上面的代码中,咱们先经过 React.createRef();建立一个ref,并赋值给组件属性textInput(this.textInput),而后在render函数中经过ref={this.textInput}的方式将ref和input这个真实DOM联系起来, 这样就能够经过 this.textInput.current.focus();的方式来直接操做input元素的方法。

不一样之处

在V16版本前,咱们能够直接经过变量访问元素的方法,在V16后,咱们须要经过 this.textInput.current,即真实的DOM是经过current属性来引用的。

若是经过 createRef()这个API赋值给组件的ref,那么引用的就是组件实例;若是是DOM元素,那引用的天然的就是DOM元素了。。

传递Refs

前面咱们说到,在V16版本以前,咱们想要父组件拿到子组件的ref,须要经过一些特殊的方法,V16版本以后,React提供了一种原生的方式来完成这种操做。

这就涉及到React新增的另外一个API: React.forwardRef(), 经过接受一个函数,来传递refs,具体以下:

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

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
  1. 首先咱们经过React.createRef();建立一个ref变量,而后在FancyButton属性中经过 ref={ref}的方式把这个ref和组件关联起来。
  2. 目前为止,若是FancyButton 是一个经过class或者函数声明的组件,那么就到此为止,咱们能够说 ref变量的current属性持有对 FancyButton组件实例的引用。
  3. 不幸的是,FancyButton通过了 React.forwardRef的处理, 这个API接受两个参数,第二个参数就是ref,而后经过 <button ref={ref}>把ref绑定到button元素上,这样ref.current的引用就是button元素这个DOM对象了。。。

上面的有点绕,简单来讲,就是咱们建立一个引用,原本是给外面的FancyButton组件的,可是由于React.forwardRef的处理,这个引用被传递给了内部的button元素。这样ref.current的引用由原本的FancyButton实例传递到了button元素自己。

在HOC组件中的应用

HOC(higher-order components)高阶组件,简单的说,就是经过组件包裹的方式来提到代码复用,高阶组件就是一个函数,且该函数接受一个组件做为参数,并返回一个新的组件。

如下是一个生成高阶组件的函数:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

logProps是函数,接受一个组件参数,返回一个包裹参数组件的logProps组件。

下面是用法:

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);

咱们先声明一个FancyButton的组件,而后将其做为参数传入logProps函数,最后获得的实际上是一个LogProps组件。

接下来咱们使用refs:

import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

咱们经过文件引入FancyButton(其实引入的是LogProps组件)而后createRef并指向FancyButton。 本意是但愿引入真正的FancyButton组件,实际上引用的是 外层包裹组件LogProps组件。

咱们能够经过如下改造来完善代码:

function logProps(Component) {
  class LogProps extends React.Component {
    render() {
      const {forwardedRef, ...rest} = this.props;

      // Assign the custom prop "forwardedRef" as a ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // Note the second param "ref" provided by React.forwardRef.
  // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
  // And it can then be attached to the Component.
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

如面的代码所示,咱们修改了高阶组件logProps函数的实现方式,在内部组件LogProps的render方法中,给被包裹组件(做为参数传入的组件)添加了来自props的ref。

最终返回的也是一个React.forwardRef处理过的组件,这个组件将ref传递到内部的props中去。

这样,但咱们经过logProps(FancyButton)函数调用的时候,其实返回的是一个通过React.forwardRef处理的组件, 当经过

<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

去添加ref的时候, 这个ref其实直接添加到了内部的LogProps组件的forwardedRef属性上,而后在LogProps组件内部,又经过props属性的方式被赋值了 被包裹组件(做为参数的组件,也就是FancyButton组件)。这个传递其实通过了三次。。。。

总的来讲,高阶组件的ref实际上是经过React.forwardRef技术将ref传递到包裹组件logProps上,而后有经过属性传递 传递到真正的FancyButton组件上,两次传递才完成。。。。

相关文章
相关标签/搜索