React中的Refs

433 阅读5分钟

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

何时使用Refs

  • 管理焦点,文本选择或媒体播放
  • 触发强制动画
  • 集成第三方DOM库

Refs类型

包括String类型的Refs、React.createRef()、回调Refs、React.useRef()

String类型的Refs(已过时,未来版本可能会被移除)

<div ref="divRef"></div>

缺陷:

  • 它不允许一个实例具有多个拥有者
  • 动态字符串可能会破坏VM中的优化

详细了解,点击 这里

React.createRef()(v16.3)

class MyComponent extends React.Component {  
    constructor(props){    
        super();    
        this.myRef = React.createRef();  
}  

render() {    
    return <div ref={this.myRef} />  
}}

// 访问ref
const node = this.myRef.current;

何时挂载:react会在组件挂载时给current属性传入DOM元素,并在组件卸载时传入null。ref会在componentDidMount或componentDidUpdate生命周期钩子触发前更新。

回调Refs

接收React组件实例或者HTML DOM元素作为参数,以使它们能在其他地方被储存和访问。

DOM元素:

<input ref={ e => this.inputRef = e } >

// 调用input元素focus()方法
this.divRef.focus()

组件实例:

<Child ref={ e => this.childRef = e }

// 调用childRef.onSubmit()方法
this.childRef.onSubmit()

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

回调refs与createRef()的不同点:

  • 回调refs能更精准的控制何时被设置和解除
  • 回调 refs 接收组件实例或 DOM 元素作为参数,而 createRef() 接收组件实例或 DOM 元素给 current 属性,多了一层 .current

useRef()

默认情况下,**不能在函数组件上使用ref属性,**因为他们没有实例。

如果你要在函数式组件中使用ref,可以使用 useRef(v16.8)。

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

在函数式组件中访问DOM:

import React, {createRef, useRef} from 'react';

export default CustomTextInput = (props) => {
    // 这里必须声明 textInput,这样 ref 才可以引用它
    const textInput = useRef(null);
    // const textInput = createRef();  // 只能保存 DOM 节点

    function handleClick() {
        textInput.current.focus();
    }

    return (
        <div>
            <input ref={textInput} />
            <input type="button" value="Focus the text input" onClick={handleClick} />
        </div>
    );
}

前面提到的访问 DOM 的两种主要方式(React.createRef() 和 回调Refs),无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。然而 useRef 比 ref 属性更有用的地方是可以很方便的保存任何可变值,其类似于 class 中使用实例字段的方法,不会因为重复 render 而重复声明

在函数式组件中访问函数组件下的 DOM,要使用 Refs 转发将其包裹然后向下传递

import React, {useRef,  forwardRef} from 'react';
const FanceInput = forwardRef((props, ref) => {
    return <input ref={ref} {...props} />;
})

const CustomTextInputSub = (props) => {
    const fancyInputRef = useRef();
    function handleClick() {
        fancyInputRef.current.focus();
    }  
    return (    
        <div>      
            <FanceInput ref={fancyInputRef} />
            <button onClick={handleClick}>btn</button>
        </div>
    );
}

export default CustomTextInputSub;

Refs转发(React.forwardRef)

refs 转发允许某些组件接收 ref,并将其向下传递给子组件。

react.forwardRef 接受一个渲染函数,其接受一个 props 和 ref 参数,并返回一个 React 节点。

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

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

在渲染函数内部,我们接收 FancyButton 传递下来的 ref 和 props,并把 ref 传递给 button 元素,这样就可以直接获取到 button 的 ref。

Refs与高阶组件

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

  return LogProps;
}

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

  // ...
}

// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton)

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

import FancyButton from './FancyButton';

const ref = React.createRef();
// 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
// 这意味着我们不能调用例如 ref.current.focus() 这样的方法
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}/>;

这时候,我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件

function logProps(Component) {
  class LogProps extends React.Component {
    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} />;  
  });
}

Ant Design 中的 Form.create

被 form.create() 包裹的组件需要使用 wrappedComponentRef 代替 ref,不然获取不到组件实例。

小结

  • string 类型的 ref 一个实例不允许具有多个拥有者
  • 回调 refs 比 React.createRef() 能更精准的控制何时被设置和解除
  • 回调 refs 接收组件实例或 DOM 元素作为参数,而 createRef() 接收组件实例或 DOM 元素给 current 属性,多了一层 .current
  • 不能在函数式组件上使用 ref 属性,因为它们没有实例,但是我们可以在函数式组件内部使用 useRef 获取 DOM,或者使用 Refs 转发将渲染函数包裹然后向下传递 ref 获取渲染函数内部的 DOM
  • useRef()只能在函数式组件内使用,相比回调 refs 和 createRef() , 它可以保存任何可变值,其类似于 class 中使用实例字段的方法,不会因为重复 render 而重复声明
  • 如果一个组件被高阶组件包裹,直接使用 refs 时获取到的是高阶组件实例,refs 转发能帮助我们我们获取到被包裹的实例,这很有用