在React中,「Ref forwarding」提供了让父组件向子组件传递“Refs”的能力:父组件

React Refs

要想理解「Ref forwarding」机制,我们首先要理解refs为何物,以及他们是如何工作的。

我们都知道,通常情况下React组件通过属性(props)向子组件传递数据。如果想修改子组件的行为状态,我们需要修改响应的属性值即可(数据驱动)。

React refs给我们提供了一种可以获取到元素所代表的DOM节点的能力:refs持有了一个DOM元素本身的引用, 通过它我们可以利用纯原生的JS函数来直接操作DOM节点,从而在不修改其属性和重新渲染该组件的前提下改变子组件的行为状态。

通俗地将,通过React refs,我们可以直接获取到指向DOM节点的引用,从而可以在不修改React组件状态的情况下(不触发重新渲染),直接操作DOM节点。

一个简单的栗子,当点击button时候Input输入框获取到焦点:

  1. export default class App extends Component {
  2. constructor(props) {
  3. super(props);
  4. this.myInput = React.createRef();
  5. }
  6. render() {
  7. return (
  8. <div>
  9. <input ref={this.myInput} />
  10. <button
  11. onClick={() => {
  12. this.myInput.current.focus();
  13. }}
  14. >
  15. focus!
  16. </button>
  17. </div>
  18. );
  19. }
  20. }

像上面例子中那样,如果我们选择自己在组件里面维护一个input框的聚焦状态,每次修改前需要判断输入框是否可以聚焦,而且每次状态的修改都会引发组件的重绘,对于这种场景明显直接使用DOM ref 更加快捷方便。同理,使用refs来直接播放器,文本选择等等,也会更方便。

那什么场景下更适合直接只用refs来操作DOM节点而不是维护组件状态呢?React的官方文档给了一些Case:「React documentation」另外,关于React refs的创建和使用,大家可以参考官方文档,这里就不多啰嗦了。

我们不仅可以将Ref绑定到一个DOM元素上,还可以将它与一个React Class组件绑定(Ref指向该组件的实例对象)。不过需要注意的是,我们无法将Ref指向一个函数式组件,因为函数式组件没有实例对象。

Forwarding Refs

聊完了refs,终于可以来讨论一下 Forwarding Refs 了。Forwording Ref 提供了一种能让父组件获取到子组件内部封装元素的能力。通俗地讲,通过Forwording Ref, 子组件可以将自己内部定义的某些元素暴露给父组件,这样父组件就可以直接操作它了:

  1. const SampleButton = React.forwardRef((props, ref) => (
  2. <button ref={ref} className="button">
  3. {props.children}
  4. </button>
  5. ));
  6. const ref = React.createRef();
  7. <SampleButton ref={ref}>Click me!</SampleButton>;

上面的栗子中,SampleButton这个子组件封装了一个button元素,为了让外部组件可以直接拿到button的引用,使用了React.forwardRef来定义它。这样,外部组件就可以通过ref.current拿到button节点的引用并进行操作了。

这时候我们会发现一个问题,直接将组件中所封装的元素引用暴露给外部去操作,是一件风险很大的事情,而且在某种程度上来说就是在破坏组件的封装性。

useImperativeHandle 这个Hook能帮助我们自定义对外暴露可以进行的操作,具体使用方式请自行参考官方文档。

Conclusion

使用React.forwardRef让外部元素在操作子组件中元素时不局限于组件对外暴露的属性状态,从而可以大大提升组件的复用性。