分享更有价值
被信任是一种快乐

React中的ref怎么使用

文章页正文上

这篇文章主要介绍“React中的ref怎么使用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“React中的ref怎么使用”文章能帮助大家解决问题。 对于 Ref 的理解,要从两个角度去分析:Ref 对象的创建:使用 createRefuseRef 创建 Ref 对象React 本身对 Ref 的处理:对于标签中的 ref 属性,React 是如何处理的在类组件中,我们会通过 createRef 去创建一个 Ref 对象,其会被保存在类组件实例上,它的实现很简单packages/react/src/ReactCreateRef.js

exportfunctioncreateRef():RefObject{
constrefObject={
current:null,
}

returnrefObject
}

可以看到,就是创建了一个包含 current 属性的对象,仅此而已这也就意味着我们不能在函数组件中使用 createRef,因为每次函数组件渲染都是一次新的函数执行,每次执行 createRef 得到的都是一个新的对象,无法保留其原来的引用所以在函数组件中,我们会使用 useRef 创建 Ref 对象,React 会将 useRef 和函数组件对应的 fiber 对象关联,将 useRef 创建的 ref 对象挂载到对应的 fiber 对象上这样一来每次函数组件执行,只要函数组件不被销毁,那么对应的 fiber 对象实例也会一直存在,所以 ref 也能够被保留下来首先要明确一个结论,在 React 中获取 DOM 元素或者组件实例并不是只能通过 ref 对象获取!!!也就是说并不是只能通过先调用 createRef 创建 ref 对象,然后将它赋值到要获取的元素或组件实例的 ref 属性上,实际上还有别的方式:::tip只有类组件才有获取组件实例这一说法,函数组件没有实例,不能被 ref 标记,但是可以通过 forwardRef 结合 useImperativeHandle 给函数组件赋予 ref 标记的:::当我们给元素或类组件标签中的 ref 属性传递字符串时,能够在组件实例的 this.refs 中访问到

classChildextendsReact.Component{
render():React.ReactNode{
const{children}=this.props

return(

Child

{children}
) } } /**@descriptionref属性传递字符串*/ classRefDemo1extendsReact.Component{ logger=createLoggerWithScope('RefDemo1') componentDidMount():void{ this.logger.log(this.refs) } render():React.ReactNode{ return(
ref属性传递字符串获取DOM元素
ref属性传递字符串获取类组件实例 > ) } }

Child:::warning这种方式已经被 React 官方废弃,尽量不要使用:::ref 属性传递函数时,会在 commit 阶段创建真实 DOM 时执行 ref 指定的函数,并将元素作为第一个参数传入,此时我们就可以利用它进行赋值以获取 DOM 元素或组件实例

/**@descriptionref属性传递函数*/
classRefDemo2extendsReact.Component{
logger=createLoggerWithScope('RefDemo2')

refDemo2DOM:HTMLElement|null=null
refDemo2Component:Child|null=null

componentDidMount():void{
this.logger.log(this.refDemo2DOM)
this.logger.log(this.refDemo2Component)
}

render():React.ReactNode{
return(

(this.refDemo2DOM=el)}> ref属性传递函数获取DOM元素
(this.refDemo2Component=child)}> ref属性传递函数获取类组件实例 > ) } }

这种方式就是我们最常用的方式了,使用 createRef 或者 useRef 创建 Ref 对象,并将其传给标签的 ref 属性即可这种方式获取到的 ref 需要先调用 current 属性才能获取到对应的 DOM 元素或组件实例

/**@descriptionref属性传递对象*/
classRefDemo3extendsReact.Component{
logger=createLoggerWithScope('RefDemo3')

refDemo3DOM=React.createRef()
refDemo3Component=React.createRef()

componentDidMount():void{
this.logger.log(this.refDemo3DOM)
this.logger.log(this.refDemo3Component)
}

render():React.ReactNode{
return(

ref属性传递对象获取DOM元素
ref属性传递对象获取类组件实例 > ) } }

想要在爷组件中通过在子组件中传递 ref 获取到孙组件的某个元素,也就是在爷组件中获取到了孙组件的元素,是一种跨层级获取

/**@description孙组件*/
constChild:React.FC}>=(props)=>{
const{grandRef}=props

return(

Child

要获取的目标元素
> ) } /** *@description父组件 * *第一个泛型参数是ref的类型 *第二个泛型参数是props的类型 */ constFather=forwardRef((props,ref)=>{ return(
) }) /**@description爷组件*/ constGrandFather:React.FC=()=>{ letgrandChildDiv:HTMLDivElement|null=null useEffect(()=>{ logger.log(grandChildDiv) },[]) return(
(grandChildDiv=el)}/>
) }

ChildforwardRef 不仅可以转发 ref 获取 DOM 元素和组件实例,还可以转发合并后的自定义 ref什么是“合并后的自定义 ref”呢?通过一个场景来看看就明白了:::info{title=场景}通过给 Foo 组件绑定 ref,获取多个内容,包括:子组件 Bar 的组件实例Bar 组件中的 DOM 元素 button孙组件 Baz 的组件实例:::这种在一个 ref 里能够访问多个元素和实例的就是“合并后的自定义 ref”

/**@description自定义ref的类型*/
interfaceCustomRef{
bar:Bar
barButton:HTMLButtonElement
baz:Baz
}

classBazextendsReact.Component{
render():React.ReactNode{
return
Baz
} } classBarextendsReact.Component }>{ buttonEl:HTMLButtonElement|null=null bazInstance:Baz|null=null componentDidMount():void{ const{customRef}=this.props if(customRef){ ;(customRefasMutableRefObject).current={ bar:this, barButton:this.buttonEl!, baz:this.bazInstance!, } } } render(){ return( (this.bazInstance=instance)}/> > ) } } constFowardRefBar=forwardRef((props,ref)=>( )) constFoo:React.FC=()=>{ constcustomRef=useRef(null) useEffect(()=>{ logger.log(customRef.current) },[]) return }

如果我们在高阶组件中直接使用 ref,它会直接指向 WrapComponent

classTestComponentextendsReact.Component{
render():React.ReactNode{
return

TestComponent

} } /**@description不使用forwardRef转发HOC中的ref*/ constHOCWithoutForwardRef=(Component:typeofReact.Component)=>{ classWrapComponentextendsReact.Component{ render():React.ReactNode{ return(

WrapComponent

) } } returnWrapComponent } constHOCComponent1=HOCWithoutForwardRef(TestComponent) constRefHOCWithoutForwardRefDemo=()=>{ constlogger=createLoggerWithScope('RefHOCWithoutForwardRefDemo') constwrapRef=useRef(null) useEffect(()=>{ //wrapRef指向的是WrapComponent实例而不是HOCComponent1实例 logger.log(wrapRef.current) },[]) return }

TestComponentWrapComponent如果我们希望 ref 指向的是被包裹的 TestComponent 而不是 HOC 内部的 WrapComponent 时该怎么办呢?这时候就可以用 forwardRef 进行转发了

/**@descriptionHOC中使用forwardRef转发ref*/
constHOCWithForwardRef=(Component:typeofReact.Component)=>{
classWrapComponentextendsReact.Component
}>{
render():React.ReactNode{
const{forwardedRef}=this.props

return(

WrapComponent

) } } returnReact.forwardRef((props,ref)=>( )) } constHOCComponent2=HOCWithForwardRef(TestComponent) constRefHOCWithForwardRefDemo=()=>{ constlogger=createLoggerWithScope('RefHOCWithForwardRefDemo') consthocComponent2Ref=useRef(null) useEffect(()=>{ //hocComponent2Ref指向的是HOCComponent2实例 logger.log(hocComponent2Ref.current) },[]) return }

WrapComponent一般我们可以通过父组件改变子组件 props 的方式触发子组件的更新渲染完成组件间通信但如果我们不希望通过这种改变子组件 props 的方式的话还能有别的办法吗?可以通过 ref 获取子组件实例,然后子组件暴露出通信的方法,父组件调用该方法即可触发子组件的更新渲染对于函数组件,由于其不存在组件实例这样的说法,但我们可以通过 useImperativeHandle 这个 hook 来指定 ref 引用时得到的属性和方法,下面我们分别用类组件和函数组件都实现一遍

/**
*父->子使用ref
*子->父使用props回调
*/
classCommunicationDemoFatherextendsReact.Component{
state:Readonly={
fatherToChildMessage:'',
childToFatherMessage:'',
}

childRef=Rea免费云主机、域名ct.createRef()

/**@description提供给子组件修改父组件中的状态*/
handleChildToFather=(message:string)=>{
this.setState((state)=>({
...state,
childToFatherMessage:message,
}))
}

constructor(props:{}){
super(props)
this.handleChildToFather=this.handleChildToFather.bind(this)
}

render():React.ReactNode{
const{fatherToChildMessage,childToFatherMessage}=this.state

return(

父组件

子组件对我说:{childToFatherMessage}

this.setState((state)=>({ ...state, fatherToChildMessage:e.target.value, })) } />
{/*父->子--使用ref完成组件通信*/}
) } } interfaceCommunicationDemoChildProps{ onChildToFather:(message:string)=>void } //子组件自己维护状态不依赖于父组件props interfaceCommunicationDemoChildState{ fatherToChildMessage:string childToFatherMessage:string } classCommunicationDemoChildextendsReact.Component{ state:Readonly={ fatherToChildMessage:'', childToFatherMessage:'', } /**@description暴露给父组件使用的API--修改父到子的消息fatherToChildMessage*/ setFatherToChildMessage(message:string){ this.setState((state)=>({...state,fatherToChildMessage:message})) } render():React.ReactNode{ const{onChildToFather:emitChildToFather}=this.props const{fatherToChildMessage,childToFatherMessage}=this.state return(

子组件

父组件对我说:{fatherToChildMessage}

this.setState((state)=>({ ...state, childToFatherMessage:e.target.value, })) } />
{/*子->父--使用props回调完成组件通信*/}
) } }

子组件对我说:{childToFatherMessage}父组件对我说:{fatherToChildMessage}使用 useImperativeHandle hook 可以让我们指定 ref 引用时能获取到的属性和方法,个人认为相比类组件的 ref,使用这种方式能够更加好的控制组件想暴露给外界的 API而不像类组件那样直接全部暴露出去,当然,如果你想在类组件中只暴露部分 API 的话,可以用前面说的合并转发自定义 ref 的方式去完成接下来我们就用 useImperativeHandle hook 改造上面的类组件实现的 demo 吧

interfaceChildRef{
setFatherToChildMessage:(message:string)=>void
}

/**
*父->子使用ref
*子->父使用props回调
*/
constCommunicationDemoFunctionComponentFather:React.FC=()=>{
const[fatherToChildMessage,setFatherToChildMessage]=useState('')
const[childToFatherMessage,setChildToFatherMessage]=useState('')

constchildRef=useRef(null)

return(

父组件

子组件对我说:{childToFatherMessage}

setFatherToChildMessage(e.target.value)} />
{/*父->子--使用ref完成组件通信*/}
setChildToFatherMessage(message)} />
) } interfaceCommunicationDemoFunctionComponentChildProps{ onChildToFather:(message:string)=>void } constCommunicationDemoFunctionComponentChild=forwardRef((props,ref)=>{ const{onChildToFather:emitChildToFather}=props //子组件自己维护状态不依赖于父组件props const[fatherToChildMessage,setFatherToChildMessage]=useState('') const[childToFatherMessage,setChildToFatherMessage]=useState('') //定义暴露给外界的API useImperativeHandle(ref,()=>({setFatherToChildMessage})) return(

子组件

父组件对我说:{fatherToChildMessage}

setChildToFatherMessage(e.target.value)} />
{/*子->父--使用props回调完成组件通信*/}
) })

子组件对我说:{childToFatherMessage}父组件对我说:{fatherToChildMessage}当我们在函数组件中如果数据更新后不希望视图改变,也就是说视图不依赖于这个数据,这个时候可以考虑用 useRef 对这种数据进行缓存为什么 useRef 可以对数据进行缓存?还记得之前说的 useRef 在函数组件中的作用原理吗?React 会将 useRef 和函数组件对应的 fiber 对象关联,将 useRef 创建的 ref 对象挂载到对应的 fiber 对象上,这样一来每次函数组件执行,只要函数组件不被销毁,那么对应的 fiber 对象实例也会一直存在,所以 ref 也能够被保留下来利用这个特性,我们可以将数据放到 useRef 中,由于它在内存中一直都是同一块内存地址,所以无论如何变化都不会影响到视图的改变:::warning{title=注意}一定要看清前提,只适用于与视图无关的数据:::我们通过一个简单的 demo 来更清楚地体会下这个应用场景假设我有一个 todoList 列表,视图上会把这个列表渲染出来,并且有一个数据 activeTodoItem 是控制当前选中的是哪个 todoItem点击 todoItem 会切换这个 activeTodoItem,但是并不需要在视图上作出任何变化,如果使用 useState 去保存 activeTodoItem,那么当其变化时会导致函数组件重新执行,视图重新渲染,但在这个场景中我们并不希望更新视图相对的,我们希望这个 activeTodoItem 数据被缓存起来,不会随着视图的重新渲染而导致其作为 useState 的执行结果重新生成一遍,因此我们可以改成用 useRef 实现,因为其在内存中一直都是同一块内存地址,这样就不会因为它的改变而更新视图了同理,在 useEffect 中如果使用到了 useRef 的数据,也不需要将其声明到 deps 数组中,因为其内存地址不会变化,所以每次在 useEffect 中获取到的 ref 数据一定是最新的

interfaceTodoItem{
id:number
name:string
}

consttodoList:TodoItem[]=[
{
id:1,
name:'coding',
},
{
id:2,
name:'eating',
},
{
id:3,
name:'sleeping',
},
{
id:4,
name:'playing',
},
]

constCacheDataWithRefDemo:React.FC=()=>{
constactiveTodoItem=useRef(todoList[0])

//模拟componentDidUpdate--如果改变activeTodoItem后组件没重新渲染,说明视图可以不依赖于activeTodoItem数据
useEffect(()=>{
logger.log('检测组件是否有更新')
})

return(
{todoList.map((todoItem)=>(
(activeTodoItem.current=todoItem)} >

{todoItem.name}

))}
) }

{todoItem.name}首先先看一个关于 callback ref 的小 Demo 来引出我们后续的内容

interfaceRefDemo8State{
counter:number
}
classRefDemo8extendsReact.Component{
state:Readonly={
counter:0,
}

el:HTMLDivElement|null=null

render():React.ReactNode{
return(
{ this.el=el console.log('this.el--',this.el) }} > refelement
) } }

为什么会执行两次?为什么第一次 this.el === null?为什么第二次又正常了?还记得 React 底层是有 render 阶段和 commit 阶段的吗?关于 ref 的处理逻辑就在 commit 阶段进行的React 底层有两个关于 ref 的处理函数 — commitDetachRefcommitAttachRef上面的 Demo 中 callback ref 执行了两次正是对应着这两次函数的调用,大致来讲可以理解为 commitDetachRef 在 DOM 更新之前执行,commitAttachRef 在 DOM 更新之后执行这也就不难理解为什么会有上面 Demo 中的现象了,但我们还是要结合源码来看看,加深自己的理解在新版本的 React 源码中它改名为了 safelyDetachRef,但是核心逻辑没变,这里我将核心逻辑简化出来供大家阅读:packages/react-reconciler/src/ReactFiberCommitWork.js

functioncommitDetachRef(current:Fiber){
//current是已经调和完了的fiber对象
constcurrentRef=current.ref

if(currentRef!==null){
if(typeofcurrentRef==='function'){
//callbackref和stringref执行时机
currentRef(null)
}else{
//objectref处理时机
currentRef.current=null
}
}
}

可以看到,就是从 fiber 中取出 ref,然后根据 callback ref、string ref、object ref 的情况进行处理并且也能看到 commitDetachRef 主要是将 ref 置为 null,这也就是为什么 RefDemo8 中第一次执行的 callback ref 中看到的 this.el 是 null 了核心逻辑代码如下:

functioncommitAttachRef(finishedWork:Fiber){
constref=finishedWork.ref
if(ref!==null){
constinstance=finishedWork.stateNode
letinstanceToUse

//处理ref来源
switch(finishedWork.tag){
//HostComponent代表DOM元素类型的tag
caseHostComponent:
instanceToUse=getPublicInstance(instance)
break

//类组件使用组件实例
default:
instanceToUse=instance
}

if(typeofref==='function'){
//callbackref和stringref
ref(instanceToUse)
}else{
//objectref
ref.current=instanceToUse
}
}
}

从上面的核心源码中能看到,对于 callback refstring ref,都是统一以函数的方式调用,将 nullinstanceToUse 传入callback ref 这样做还能理解,但是为什么 string ref 也是这样处理呢?因为当 React 检测到是 string ref 时,会自动绑定一个函数用于处理 string ref,核心源码逻辑如下:packages/react-reconciler/src/ReactChildFiber.js

//从元素上获取ref
constmixedRef=element.ref
conststringRef=''+mixedRef
constref=function(value){
//resolvedInst就是组件实例
constrefs=resolvedInst.refs

if(value===null){
deleterefs[stringRef]
}else{
refs[stringRef]=value
}
}

这样一来 string ref 也变成了一个函数了,从而可以在 commitDetachRefcommitAttachRef 中被执行,并且也能印证为什么 string ref 会在类组件实例的 refs 属性中获取到为什么在 RefDemo8 中我们每次点击按钮时都会触发 commitDetachRefcommitAttachRef 呢?这就需要聊聊 ref 的执行时机了,而从上文也能够了解到,ref 底层实际上是由 commitDetachRefcommitAttachRef 在处理核心逻辑那么我们就得来看看这两个函数的执行时机才能行packages/react-reconciler/src/ReactFiberCommitWork.js

functioncommitMutationEffectsOnFiber(
finishedWork:Fiber,
root:FiberRoot,
lanes:Lanes,
){
constcurrent=finishedWork.alternate
constflags=finishedWork.flags

if(flags&Ref){
if(current!==null){
//也就是commitDetachRef
safelyDetachRef(current,current.return)
}
}
}

packages/react-reconciler/src/ReactFiberCommitWork.js

functioncommitLayoutEffectOnFiber(
finishedRoot:FiberRoot,
current:Fiber|null,
finishedWork:Fiber,
committedLanes:Lanes,
){
constflags=finishedWork.flags

if(flags&Ref){
safelyAttachRef(finishedWork,finishedWork.return)
}
}

可以看到,只有当 fiber 被打上了 Ref 这个 flag tag 时才会去执行 commitDetachRef/commitAttachRef那么什么时候会标记 Ref tag 呢?packages/react-reconciler/src/ReactFiberBeginWork.js

functionmarkRef(current:Fiber|null,workInProgress:Fiber){
constref=workInProgress.ref

if(
//current===null意味着是初次挂载,fiber首次调和时会打上Reftag
(current===null&&ref!==null)||
//current!==null意味着是更新,此时需要ref发生了变化才会打上Reftag
(current!==null&&current.ref!==ref)
){
//ScheduleaRefeffect
workInProgress.flags|=Ref
}
}

那么现在再回过头来思考 RefDemo8 中为什么每次点击按钮都会执行 commitDetachRefcommitAttachRef 呢?注意我们使用 callback ref 的时候是如何使用的

{ this.el=el console.log('this.el--',this.el) }} > refelement

是直接声明了一个箭头函数,这样的方式会导致每次渲染这个 div 元素时,给 ref 赋值的都是一个新的箭头函数,尽管函数的内容是一样的,但内存地址不同,因而 current.ref !== ref 这个判断条件会成立,从而每次都会触发更新那么要如何解决这个问题呢?既然我们已经知道了问题的原因,那么就好说了,只要让每次赋值给 ref 的函数都是同一个就可以了呗~

constlogger=createLoggerWithScope('RefDemo9')

interfaceRefDemo9Props{}
interfaceRefDemo9State{
counter:number
}
classRefDemo9extendsReact.Component{
state:Readonly={
counter:0,
}

el:HTMLDivElement|null=null

constructor(props:RefDemo9Props){
super(props)
this.setElRef=this.setElRef.bind(this)
}

setElRef(el:HTMLDivElement|null){
this.el=el
logger.log('this.el--',this.el)
}

render():React.ReactNode{
return(
refelement
) } }

关于“React中的ref怎么使用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注云技术行业资讯频道,小编每天都会为大家更新不同的知识点。

相关推荐: vue中mounted和created有哪些区别

本篇内容介绍了“vue中mounted和created有哪些区别”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!区别:1、created在模板渲染成html前调用…

文章页内容下
赞(0) 打赏
版权声明:本站采用知识共享、学习交流,不允许用于商业用途;文章由发布者自行承担一切责任,与本站无关。
文章页正文下
文章页评论上

云服务器、web空间可免费试用

宝塔面板主机、支持php,mysql等,SSL部署;安全高速企业专供99.999%稳定,另有高防主机、不限制内容等类型,具体可咨询QQ:360163164,Tel同微信:18905205712

主机选购导航云服务器试用

登录

找回密码

注册