React Ref 原理理解

Posted YuLong~W

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Ref 原理理解相关的知识,希望对你有一定的参考价值。

Ref 对象创建

①类组件React.createRef

创建了一个对象,对象上的 current 属性,用于保存通过 ref 获取的 DOM 元素,组件实例等。

②函数组件 useRef

用 hooks 中的 useRef 来达到同样的效果

区别:

ref 保存位置不相同

  • 类组件有一个实例 instance 能够维护像 ref 这种信息
  • 函数组件每次更新都是一次新的开始,所有变量重新声明,ref 就会随着函数组件执行被重置

解决:hooks 和函数组件对应的 fiber 对象建立起关联,将 useRef 产生的 ref 对象挂到函数组件对应的 fiber 上,函数组件每次执行,只要组件不被销毁,函数组件对应的 fiber 对象一直存在,所以 ref 等信息就会被保存下来。

类组件获取 Ref 三种方式

① Ref属性是一个字符串

React 在底层逻辑,会判断字符串 ref 标记的类型,如果是 DOM 元素,会把真实 DOM 绑定在组件 this.refs (组件实例下的 refs )属性上,如果是类组件,会把子组件的实例绑定在 this.refs 上。

② Ref 属性是一个函数

ref 标记类型是函数的时候,将作为 callback 形式,等到真实 DOM 创建阶段,执行 callback ,获取的 DOM 元素或组件实例,将以回调函数第一个参数形式传入,

③ Ref属性是一个ref对象

Ref 高阶用法

1、forwardRef 转发 Ref

1)跨层级获取:比如想要通过标记子组件 ref ,来获取孙组件的某一 DOM 元素,或者是组件实例。

2)合并转发ref:可以用来传递合并之后的自定义的 ref。forwardRef 让 ref 可以通过 props 传递,那么如果用 ref 对象标记的 ref ,那么 ref 对象就可以通过 props 的形式,提供给子孙组件消费,当然子孙组件也可以改变 ref 对象里面的属性

3)高阶组件转发

2、ref实现组件通信

父组件可以通过 ref 模式标记子组件实例,从而操纵子组件方法

例如:参考 antd 里面的 form 表单,暴露出对外的 resetFieldssetFieldsValue 等接口,可以通过表单实例调用这些 API 。

① 类组件 ref

可以通过 ref 直接获取组件实例,实现组件通信。

如下:

  • 1 子组件暴露方法 fatherSay 供父组件使用,父组件通过调用方法可以设置子组件展示内容。
  • 2 父组件提供给子组件 toFather,子组件调用,改变父组件展示内容,实现父 <-> 子 双向通信。
/* 子组件 */
class Son extends React.PureComponent
    state=
       fatherMes:'',
       sonMes:''
    
    fatherSay=(fatherMes)=> this.setState( fatherMes  ) /* 提供给父组件的API */
    render()
        const  fatherMes, sonMes  = this.state
        return <div className="sonbox" >
            <div className="title" >子组件</div>
            <p>父组件对我说: fatherMes </p>
            <div className="label" >对父组件说</div> <input  onChange=(e)=>this.setState( sonMes:e.target.value )   className="input"  /> 
            <button className="searchbtn" onClick= ()=> this.props.toFather(sonMes)   >to father</button>
        </div>
    


/* 父组件 */
export default function Father()
    const [ sonMes , setSonMes ] = React.useState('') 
    const sonInstance = React.useRef(null) /* 用来获取子组件实例 */
    const [ fatherMes , setFatherMes ] = React.useState('')
    const toSon =()=> sonInstance.current.fatherSay(fatherMes) /* 调用子组件实例方法,改变子组件state */
    return <div className="box" >
        <div className="title" >父组件</div>
        <p>子组件对我说: sonMes </p>
        <div className="label" >对子组件说</div> <input onChange= (e) => setFatherMes(e.target.value)   className="input"  /> 
        <button className="searchbtn"  onClick=toSon  >to son</button>
        <Son ref=sonInstance toFather=setSonMes />
    </div>

② 函数组件 forwardRef + useImperativeHandle

useImperativeHandle 接受三个参数:

  • 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
  • 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
  • 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。

3、函数组件缓存数据

函数组件每一次 render ,函数上下文会重新执行,那么有一种情况就是,在执行一些事件方法改变数据或者保存新数据的时候,有没有必要更新视图,有没有必要把数据放到 state 中。这时候更新无疑是一种性能上的浪费。

useRef 可以创建出一个 ref 原始对象,只要组件没有销毁,ref 对象就一直存在,那么完全可以把一些不依赖于视图更新的数据储存到 ref 对象中。这样做的好处有两个:

  • 第一个能够直接修改数据,不会造成函数组件冗余的更新作用。
  • 第二个 useRef 保存数据,如果有 useEffect ,useMemo 引用 ref 对象中的数据,无须将 ref 对象添加成 dep 依赖项,因为 useRef 始终指向一个内存空间,所以这样一点好处是可以随时访问到变化后的值。

Ref 原理揭秘

对于整个 Ref 的处理,都是在 commit 阶段发生的,commit 阶段会进行真正的 Dom 操作,此时 ref 就是用来获取真实的 DOM 以及组件实例的。

React 底层用两个方法处理:commitDetachRefcommitAttachRef

这两次正好,一次在 DOM 更新之前,一次在 DOM 更新之后。

  • 第一阶段:一次更新中,在 commit 的 mutation 阶段, 执行commitDetachRef,commitDetachRef 会清空之前ref值,使其重置为 null。

  • 第二阶段:DOM 更新阶段,这个阶段会根据不同的 effect 标签,真实的操作 DOM 。

  • 第三阶段:layout 阶段,在更新真实元素节点之后,此时需要更新 ref 。

更新ref

只有在 Ref tag 存在的时候才会更新 ref

markRef 方法执行在两种情况下:

  • 第一种就是类组件的更新过程中。
  • 第二种就是更新 HostComponent 的时候,比如 <div /> 等元素。

markRef 会在以下两种情况下给 effectTag 标记 Ref,只有标记了 Ref tag 才会有后续的 commitAttachRefcommitDetachRef 流程

  • 第一种current === null && ref !== null:就是在 fiber 初始化的时候,第一次 ref 处理的时候,是一定要标记 Ref 的。
  • 第二种current !== null && current.ref !== ref:就是 fiber 更新的时候,但是 ref 对象的指向变了。

卸载ref

被卸载的 fiber 会被打成 Deletion effect tag ,然后在 commit 阶段会进行 commitDeletion 流程。对于有 ref 标记的 ClassComponent (类组件) 和 HostComponent (元素),会统一走 safelyDetachRef 流程

  • 对于字符串 ref="dom" 和函数类型 ref=(node)=> this.node = node 的 ref,会执行传入 null 置空 ref 。
  • 对于 ref 对象类型,会清空 ref 对象上的 current 属性。

总结

1、Ref对象创建(2种)、ref方法获取(3种)

2、forwardRef、组件通信、缓存数据

3、Ref处理逻辑

以上是关于React Ref 原理理解的主要内容,如果未能解决你的问题,请参考以下文章

React Ref 原理理解

React中父组件如何调用子组件的方法?useImperativeHandle就够了,原理级详解

c#中的ref用法

我对 React 实现原理的理解

我对 React 实现原理的理解

手写React的Fiber架构,深入理解其原理