写点什么

React 之 Refs 的使用和 forwardRef 的源码解读

作者:冴羽
  • 2022-12-05
    浙江
  • 本文字数:3720 字

    阅读完需:约 12 分钟

React 之 Refs 的使用和 forwardRef 的源码解读

三种使用方式

React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素。


React 提供了三种使用 Ref 的方式:

1. String Refs

class App extends React.Component {    constructor(props) {        super(props)    }    componentDidMount() {        setTimeout(() => {             // 2. 通过 this.refs.xxx 获取 DOM 节点             this.refs.textInput.value = 'new value'        }, 2000)    }    render() {        // 1. ref 直接传入一个字符串        return (            <div>              <input ref="textInput" value='value' />            </div>        )    }}
root.render(<App />);
复制代码

2. 回调 Refs

class App extends React.Component {    constructor(props) {        super(props)    }    componentDidMount() {        setTimeout(() => {              // 2. 通过实例属性获取 DOM 节点              this.textInput.value = 'new value'        }, 2000)    }    render() {        // 1. ref 传入一个回调函数        // 该函数中接受 React 组件实例或 DOM 元素作为参数        // 我们通常会将其存储到具体的实例属性(this.textInput)        return (            <div>              <input ref={(element) => {                this.textInput = element;              }} value='value' />            </div>        )    }}
root.render(<App />);
复制代码

3. createRef

class App extends React.Component {    constructor(props) {        super(props)        // 1. 使用 createRef 创建 Refs        // 并将 Refs 分配给实例属性 textInputRef,以便在整个组件中引用        this.textInputRef = React.createRef();    }    componentDidMount() {        setTimeout(() => {            // 3. 通过 Refs 的 current 属性进行引用            this.textInputRef.current.value = 'new value'        }, 2000)    }    render() {        // 2. 通过 ref 属性附加到 React 元素        return (            <div>              <input ref={this.textInputRef} value='value' />            </div>        )    }}
复制代码


这是最被推荐使用的方式。

两种使用目的

Refs 除了用于获取具体的 DOM 节点外,也可以获取 Class 组件的实例,当获取到实例后,可以调用其中的方法,从而强制执行,比如动画之类的效果。


我们举一个获取组件实例的例子:


class Input extends React.Component {    constructor(props) {        super(props)        this.textInputRef = React.createRef();    }    handleFocus() {        this.textInputRef.current.focus();    }    render() {        return <input ref={this.textInputRef} value='value' />    }}
class App extends React.Component { constructor(props) { super(props) this.inputRef = React.createRef(); } componentDidMount() { setTimeout(() => { this.inputRef.current.handleFocus() }, 2000) } render() { return ( <div> <Input ref={this.inputRef} value='value' /> </div> ) }}
复制代码


在这个例子中,我们通过 this.inputRef.current 获取到 Input 组件的实例,并调用了实例的 handleFocus 方法,在这个方法中,又通过 Refs 获取到具体的 DOM 元素,执行了 focus 原生方法。

forwardRef

注意在这个例子中,我们的 Input 组件使用的是类组件,Input 组件可以改为使用函数组件吗?


答案是不可以,我们不能在函数组件上使用 ref 属性,因为函数组件没有实例。


如果我们强行使用,React 会报错并提示我们用 forwardRef:


function Input() {  return <input value='value' />}
class App extends React.Component { constructor(props) { super(props) this.inputRef = React.createRef(); } render() { return ( <div> <Input ref={this.inputRef} value='value' /> </div> ) }}
复制代码



但是呢,对于“获取组件实例,调用实例方法”这个需求,即使使用 forwardRef 也做不到,借助 forwardRef 后,我们也就是跟类组件一样,可以在组件上使用 ref 属性,然后将 ref 绑定到具体的 DOM 元素或者 class 组件上,也就是我们常说的 Refs 转发。

Refs 转发

有的时候,我们开发一个组件,这个组件需要对组件使用者提供一个 ref 属性,用于让组件使用者获取具体的 DOM 元素,我们就需要进行 Refs 转发,我们通常的做法是:


// 类组件class Child extends React.Component {    render() {        const {inputRef, ...rest} = this.props;        // 3. 这里将 props 中的 inputRef 赋给 DOM 元素的 ref        return <input ref={inputRef} {...rest} placeholder="value" />    }}// 函数组件function Child(props) {      const {inputRef, ...rest} = props;      // 3. 这里将 props 中的 inputRef 赋给 DOM 元素的 ref      return <input ref={inputRef} {...rest} placeholder="value" />}
class Parent extends React.Component { constructor(props) { super(props) // 1. 创建 refs this.inputRef = React.createRef(); } componentDidMount() { setTimeout(() => { // 4. 使用 this.inputRef.current 获取子组件中渲染的 DOM 节点 this.inputRef.current.value = 'new value' }, 2000) } render() { // 2. 因为 ref 属性不能通过 this.props 获取,所以这里换了一个属性名 return <Child inputRef={this.inputRef} /> }}
复制代码


React 提供了 forwardRef 这个 API,我们直接看使用示例:


// 3. 子组件通过 forwardRef 获取 ref,并通过 ref 属性绑定 React 元素const Child = forwardRef((props, ref) => (  <input ref={ref} placeholder="value" />));
class Parent extends React.Component { constructor(props) { super(props) // 1. 创建 refs this.inputRef = React.createRef(); } componentDidMount() { setTimeout(() => { // 4. 使用 this.inputRef.current 获取子组件中渲染的 DOM 节点 this.inputRef.current.value = 'new value' }, 2000) } render() { // 2. 传给子组件的 ref 属性 return <Child ref={this.inputRef} /> }}
复制代码


尤其是在我们编写高阶组件的时候,往往要实现 refs 转发。我们知道,一个高阶组件,会接受一个组件,返回一个包裹后的新组件,从而实现某种功能的增强。


但也正是如此,我们添加 ref,获取的会是包裹后的新组件的实例,而非被包裹的组件实例,这就可能会导致一些问题。

createRef 源码

现在我们看下 createRef 的源码,源码的位置/packages/react/src/ReactCreateRef.js,代码其实很简单,就只是返回了一个具有 current 属性的对象:


// 简化后export function createRef() {  const refObject = {    current: null,  };  return refObject;}
复制代码


在渲染的过程中,refObject.current 会被赋予具体的值。

forwardRef 源码

那 forwardRef 源码呢?源码的位置/packages/react/src/ReactForwardRef.js,代码也很简单:


// 简化后const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref');
export function forwardRef(render) { const elementType = { $$typeof: REACT_FORWARD_REF_TYPE, render, };
return elementType;}
复制代码


但是要注意这里的 $$typeof,尽管这里是 REACT_FORWARD_REF_TYPE,但最终创建的 React 元素的 $$typeof 依然为 REACT_ELEMENT_TYPE


关于 createElement 的源码分析参考 《React 之 createElement 源码解读》,我们这里简单分析一下,以 InputComponent 为例:


// 使用 forwardRefconst InputComponent = forwardRef(({value}, ref) => (  <input ref={ref} className="FancyButton" value={value} />));
// 根据 forwardRef 的源码,最终返回的对象格式为:const InputComponent = { $$typeof: REACT_FORWARD_REF_TYPE, render,}
// 使用组件const result = <InputComponent /> // Bable 将其转译为:const result = React.createElement(InputComponent, null);
// 最终返回的对象为:const result = { $$typeof: REACT_ELEMENT_TYPE, type: { $$typeof: REACT_FORWARD_REF_TYPE, render, }}
复制代码


我们尝试着打印一下最终返回的对象,确实也是这样的结构:


React 系列

讲解 React 源码、React API 背后的实现机制,React 最佳实践、React 的发展与历史等,预计 50 篇左右,欢迎关注


如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

发布于: 刚刚阅读数: 3
用户头像

冴羽

关注

理想主义者 2019-01-25 加入

17 年开始写前端文章,至今 6 个系列,上百篇文章,全网百万阅读 wx 加 mqyqingfeng,进读者群,看不一样的世界 公众号搜索 「yayujs」 GitHub 26K Star: https://github.com/mqyqingfeng/Blog

评论

发布
暂无评论
React 之 Refs 的使用和 forwardRef 的源码解读_JavaScript_冴羽_InfoQ写作社区