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 表单,暴露出对外的 resetFields
, setFieldsValue
等接口,可以通过表单实例调用这些 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 底层用两个方法处理:commitDetachRef 和 commitAttachRef
这两次正好,一次在 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 才会有后续的 commitAttachRef
和 commitDetachRef
流程
- 第一种
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 原理理解的主要内容,如果未能解决你的问题,请参考以下文章