使用过Vue
框架的都会对ref
非常熟悉,我们经常会使用它来引用 一些组件实例或者DOM实例,而且在vue中使用起来也非常简单方便。但是在React
中,ref
使用起来要稍微复杂一些,并且对于不同的组件类型我们还需要使用不同的API
。
下面我们就开始学习react
框架中ref
系列API
的用法及原理。
1,ref 的创建
createRef
(一)用法
在类组件的constructor
构造器中使用createRef
方法创建一个ref
对象,可以绑定到DOM节点或者子组件上,用于获取DOM实例或者类组件实例。
js
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super()
this.Ref1 = React.createRef();
this.Ref2 = React.createRef();
}
render() {
return (
<div ref={this.ref1}>App组件</div>
// 类组件
<Child ref={this.ref2}></Child>
)
}
}
(二)原理
查看createRef
方法源码:
js
// react/src/ReactCreateRef.js
export function createRef(): RefObject {
const refObject = {
current: null,
};
// 返回一个对象 { current: null }
return refObject;
}
可以看出来createRef
方法源码非常简单,就是直接返回一个新建的对象{ current: null }
,最终这个current
属性会在react渲染流程的commit
阶段中被赋值为DOM实例或者类组件实例。
js
constructor() {
super()
this.Ref1 = React.createRef();
// 自定义
this.Ref2 = {
current: null
}
}
所以说,在类组件中我们完全可以自定义一个ref
对象,就可以达到一样的效果。
同时根据源码我们还可以看出:createRef
方法的缺点就是多次调用会多次新建ref
对象 ,当然这也并不是它的缺点,因为它的设计原本就是在类组件中使用,因为类组件只有在首次加载时才会执行constructor
构造器的,创建一个组件实例instance
,在更新阶段只会更新组件实例的内容,即只会在创建时执行一次createRef
方法,但是函数组件每次渲染都会重新调用组件函数,所以如果在函数组件中使用createRef
方法,就会导致每次渲染都会创建一个新的ref
对象,而使用此对象的子节点props
会一直变化,造成额外的更新浪费,所以这个方法只适合在类组件中使用。
useRef
(一)用法
在函数组件中使用useRef
创建一个固定引用的ref
对象,可以绑定到组件内的DOM节点之上。
js
import { useRef } from 'react'
export default function App() {
const inpRef = useRef();
return (
<div>
<input ref={inpRef} type="text" />
</div>
)
}
(二)原理
- 函数组件加载阶段:
js
const HooksDispatcherOnMount: Dispatcher = {
useRef: mountRef,
}
查看mountRef
方法:
js
function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
mountRef
方法就是useRef hook
在函数组件加载时实际调用的方法,可以看出此方法也是比较简单的,这里的mountWorkInProgressHook
方法作用是创建一个react内部的hook
对象,并按顺序构建一个hook
链表,具体的逻辑这里不会展开,感兴趣的可以查看《React18.2x源码解析:函数组件的加载过程》。
然后同样是创建一个ref
对象{ current: null }
,并且将这个对象存储到了hook
对象的memoizedState
属性上。
最后返回新建的ref
对象。
- 函数组件更新阶段:
js
const HooksDispatcherOnUpdate: Dispatcher = {
useRef: updateRef,
}
查看updateRef
方法:
js
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
这里的updateWorkInProgressHook
方法主要作用是拿到函数组件加载时对应的hook
对象信息,然后直接返回hook.memoizedState
属性内容,也就是之前的ref
对象。
原理总结: useRef
在函数组件加载时会创建一个ref
对象{ current: null }
,存储到内部hook
对象的memoizedState
属性之上,而在组件更新时会直接返回这个对象,即一直是引用的同一个对象,所以函数组件每次重新渲染不会再创建新的ref
对象。
注意:每一个hook钩子在函数组件加载时都会在react内部创建一个对应的
hook
对象,存储自身的相关信息,这些hook对象会存储到组件节点Fiber
对象上,所以只要组件还存在,就能保证一直是在使用同一个ref
对象。
2,ref 的更新
前面我们理解了ref
的创建,不管哪一个API
最终都会创建出一个ref
对象:
js
{ current: null }
而最终绑定的DOM实例或者类组件实例会被存储到current
属性上,下面我们继续了解更新ref
对象的过程。
这里分为两种情况:
- 类组件实例
- DOM实例
因为ref
能直接绑定的只有这两种,函数组件无法直接绑定ref
,只能通过forwardRef
转发,我们放到最后再讲解。
类组件实例
在类组件中拿到子组件的实例:
js
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super()
this.Ref = React.createRef();
}
// 打印ref对象
handleClick = () => {
console.log(this.ref)
}
render() {
return (
// 类组件
<div>
<Child ref={this.ref}></Child>
<button onClick={this.handleClick}>打印ref</button>
</div>
)
}
}
reconciler协调流程
类组件在创建子节点时,首先会调用组件实例的render
方法,将return
返回的jsx
内容转化为react元素对象 【react-element
】,如果子节点存在ref
绑定,创建的react
元素对象就会保存上初始的ref
对象{current: null}
,如下图所示:
然后创建child
子组件对应的Fiber
节点时就会将这个ref
对象存储到Fiber.ref
属性上:
到此,child
子组件对应的Fiber
节点创建完成,其绑定的ref
对象也初始化完成。
下面来到child
组件Fiber
节点的beginWork
工作流程【类组件加载核心流程】:
js
// 类组件的加载
updateClassComponent() {
...
const nextUnitOfWork = finishClassComponent()
}
js
function finishClassComponent() {
// 标记ref更新
markRef(current, workInProgress);
}
updateClassComponent
就是类组件加载的核心函数,在这个函数的最后会调用一个finishClassComponent
方法,这个方法无论组件加载阶段还是更新阶段都会调用,而这个方法的第一行代码就是处理ref
对象。
我们继续查看markRef
方法:
js
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.flags |= Ref;
}
}
current
代表旧的Fiber
节点。workInProgress
代表当前正常处理中的新的Fiber
节点。
当前我们是child
组件的加载阶段,所以current
就会为null
,并且我们的ref
对象是存在的,所以满足下面的第一个判断条件,此时就会给当前的组件Fiber
节点即workInProgress
打上ref
的副作用标记。它的作用就是在后面的commit
阶段中来更新ref
对象,即为ref
对象的current
属性赋值为真实的组件实例。
扩展: 这里我们可以注意第二个判断,它针对的是组件更新阶段的处理:
js
(current !== null && current.ref !== ref)
组件更新时current
肯定不为null
,这里重点是第二个条件,判断原来的ref
对象和最新的ref
是否相等,即是否为同一个对象。
- 如果相等则表示
ref
对象没有变化,不需要标记ref
副作用,在之后的commit
阶段就不会更新ref
的绑定。 - 如果不相等则表示
ref
对象发生变化,需要标记ref
副作用,在之后的commit
阶段就会更新ref
的绑定。
注意: 如果你将ref
绑定为一个箭头函数:
js
ref = {() => {}}
则每次给ref
赋值的都是一个新的箭头函数【函数地址不同】,就会造成每次组件更新都需要重新绑定ref
。
commit阶段
上面child
组件的Fiber
节点已经标记了ref
副作用,下面直接来到commit
阶段进行ref
的绑定。
处理类组件ref
绑定的逻辑在commit
阶段中的Layout
子阶段中,但是绑定之前,还有一个解绑的处理,我们需要先理解这个逻辑,因为这个逻辑每次都会在绑定之前执行。
- 解绑的处理【或者说重置
ref
】:Mutation
阶段。
js
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
switch (finishedWork.tag) {
...
// 类组件处理
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// ref重置处理
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
}
}
在Mutation
阶段中处理类组件的逻辑时,会有一个解绑ref
的操作,当然此类组件Fiber
节点必须得有ref
的副作用标记,然后这里有一个current
不为null
的判断,也就是说解绑【或者说重置ref
】的逻辑必须得在组件的更新阶段才会执行,其实这也正常,因为在组件的加载阶段,ref
对象默认就是初始化的状态{current: null}
,不需要再重置的操作。
js
safelyDetachRef(current, current.return);
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === 'function') {
// 如果ref为一个函数,直接调用ref,传递一个null,来重置
ref(null);
} else {
ref.current = null;
}
}
也就是在每次真正的绑定ref
对象之前,都会先重置ref
为初始的状态。
- 绑定的处理:
Layout
阶段。
js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
// 根据组件类型
switch (finishedWork.tag) {
...
}
}
// 绑定ref
if (!offscreenSubtreeWasHidden) {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}
}
在每个Fiber
节点执行完Layout
阶段相关的副作用之后,最后都会有一个关于ref
绑定的处理逻辑。
这里得满足两个判断条件才能够执行绑定ref
的方法:
- 当前节点没有被隐藏在屏幕之外。
- 当前节点存在
ref
的副作用标记。
只有同时满足这个两个条件,才能绑定ref
,当前我们的条件都满足,直接查看commitAttachRef
方法。
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
if (typeof ref === 'function') {
// 如果ref为一个函数,则直接将DOM实例或者组件实例作为参数传递给函数
ref(instance);
} else {
ref.current = instance;
}
}
}
这里的绑定ref
就是直接从当前Fiber
对象的stateNode
属性中,取出对应的类组件实例或者DOM实例。
注意:
- 类组件
Fiber
节点的stateNode
属性存储的是组件实例instance
。 - 普通DOM
Fiber
节点的stateNode
属性存储的是DOM元素实例instance
。
当前我们为类组件,就会取出对应的类组件实例,然后下面还有一个判断:
- 如果
ref
为一个函数,则直接将DOM实例或者组件实例作为参数传递给函数。 - 否则
ref
就是一个对象,则直接将组件实例赋值给current
属性。
到此,ref
对象的绑定逻辑就执行完成。
最后我们点击打印按钮,查看绑定完成的ref
对象:
DOM实例
在函数组件中拿到DOM元素实例:
js
import { useRef } from 'react'
export default function MyFun() {
const ref = useRef()
function handleClick() {
console.log(ref)
}
return (
<div className='MyFun'>
<div ref={ref}>DOM实例</div>
<button onClick={this.handleClick}>打印ref</button>
</div>
)
}
reconciler协调流程
函数组件加载时会调用一次我们定义的函数即MyFun
,最后会触发return
关键字,将整个jsx
内容都转化为react元素对象 【react-element
】后返回,最外面的元素为<div className='MyFun'>
对应的DOM内容,所以返回的就是它对应的react元素对象:
因为我们绑定ref
的DOM不是最外层的,而是它的子节点,所以这里我们可以查看它的children
属性:
js
<div ref={ref}>DOM实例</div>
然后目标DOM对应的react元素对象:
此时,DOM节点创建的react
元素对象就会保存上初始的ref
对象{current: null}
。
后面依然是执行和前面相同的逻辑,当DOM节点对应的Fiber
节点创建时,就会将这个ref
对象存储到Fiber.ref
属性上:
最后来到DOM节点组件对应的completeWork
工作流程中。
- 类组件标记
ref
的操作在beginWork
工作流程中。 - DOM节点组件即
hostComponent
标记ref
的操作在completeWork
工作流程中。
注意:每个
FIber
节点的创建处理都分为beginWork
和completeWork
两个工作流程。
js
case HostComponent: {
...
if (current !== null)
// 更新阶段
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// 加载阶段
if (workInProgress.ref !== null) {
markRef(workInProgress);
}
}
}
current
不为null
则表示为更新阶段,此时标记ref
更新时判断必须是ref
存在变化:
js
if (current.ref !== workInProgress.ref)
即原来的ref
对象和现在的ref
对象必须不相等,才会标记ref
副作用,在后面的commit
中就可以执行ref
的更新操作。如果ref
对象没有变化则不会执行更新操作。注意这里的ref
并不一定是对象,就前面讲过的有可能是箭头函数,但它们的判断是一样的。
当前为加载阶段,只需要判断Fiber.ref
是否存在,当前是存在的,所以这里就会对Fiber
打上ref
的副作用标记。
js
function markRef(workInProgress: Fiber) {
workInProgress.flags |= Ref;
}
commit阶段
上面DOM节点组件的Fiber
节点已经标记了ref
副作用,下面直接来到commit
阶段进行ref
的绑定。
commit
阶段对ref
的处理和上面类组件的讲解是完全一样的:
- 依然是先调用一次
safelyDetachRef
,重置ref
对象为初始状态。 - 然后调用
commitAttachRef
方法,进行真实的DOM实例绑定。
所以这里我们就不重复讲述,最后直接点击打印按钮,查看绑定完成的ref
对象:
3,ref 的转发
上面我们已经理解了类组件和DOM节点的ref
原理,其实也并不复杂,最后我们再来学习一下函数组件对ref
的处理,因为函数组件不能直接绑定ref
,所以需要通过forwardRef
来进行转发处理。
forwardRef
(一)用法
可以通过forwardRef
包装我们的函数组件,将ref
传递到函数内部绑定的到目标DOM节点上,这样我们便可以在父组件中拿到子组件内部的目标DOM实例。
在实际的应用里,我们使用
forwardRef
时会更多的与useImperativeHandle hook
搭配使用。
js
// App.js
import { useRef } from 'react'
function App() {
const ref = useRef();
function handleClick() {
console.log(ref)
}
return (
<div className='App'>
<div>App组件</div>
<NewChild ref={ref}></NewChild>
<button onClick={handleClick}>打印ref</button>
</div>
)
}
js
// child.js
import React from 'react'
function Child(props, ref) {
return (
<div className='Child'>
<div ref={ref}>DOM实例</div>
</div>
)
}
export default React.forwardRef(Child)
(二)原理
查看forwardRef
方法源码:
js
// packages\react\src\ReactForwardRef.js
export function forwardRef<Props, ElementType: React$ElementType>(
render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
// 定义一个新的组件,包装传入的函数组件
const elementType = {
// 存储组件的类型为:'react.forward_ref'
$$typeof: REACT_FORWARD_REF_TYPE,
render, // 传入的组件,一个render函数
};
// 返回包装后的组件
return elementType;
}
可以看出forwardRef
方法源码非常简单,仅仅是创建了一个内部组件,来包装我们传入的函数组件【render
函数】,最后返回的包装后的组件【forward_ref
类型组件】,这里elementType
只有两个属性:
$$typeof
:组件的类型标识:为react.forward_ref
,用于组件渲染时特殊处理。render
:存储传入的函数组件。
上面的NewChild
就是forwardRef
组件,我们首先查看它对应的react
元素对象:
我们可以看出forwardRef
类型组件和普通函数组件基本相同,它们都是react元素对象【react-element
】,唯一的区别是两个对象的type
属性不同。forwardRef
类型组件创建的react元素对象,它的type
属性为一个对象,而函数组件创建的react元素对象,它的type
属性为函数组件自身【即为一个函数】,如图所示:
上面我们已经知道了forwardRef
类型组件的react元素对象内容,并且ref
也已经被初始化。
下面我们直接进入到forwardRef
组件的加载逻辑:
js
case ForwardRef: {
// 取出type对象
const type = workInProgress.type;
return updateForwardRef(current, workInProgress,type,...);
}
这里取出Fiber
节点的type
属性,然后传递给updateForwardRef
方法:
下面我们直接查看updateForwardRef
方法:
js
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
const render = Component.render;
const ref = workInProgress.ref;
// 调用render
nextChildren = renderWithHooks(
current,
workInProgress, // 当前forwardRef组件的Fiber
render, // 传入的函数组件
nextProps,
ref, // { current: null }
renderLanes,
);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
注意: 这里的Component
参数就是上面的type
对象,取出对象里面的render
函数,就是我们的child
组件函数。
然后从当前forwardRef
组件对应的Fiber
节点中取出初始的ref
对象:
最后将这个参数传递给renderWithHooks
方法,开始Child
函数组件的加载。
注意:这里虽然是加载的
Child
函数组件的内容,但是当前的Fiber
节点是forwardRef
组件,也是就说当我们用forwardRef
包裹了函数组件之后,就不会存在函数组件的Fiber
节点了,只有forwardRef
组件对应的Fiber
节点,而下一个Fiber
节点是组件内的根DOM节点。
继续查看renderWithHooks
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any, // render函数
props: Props,
secondArg: SecondArg, // ref
nextRenderLanes: Lanes,
): any {
...
// 调用函数
let children = Component(props, secondArg);
...
}
renderWithHooks
方法就是函数组件真正的加载逻辑,这里我们只关注这一行代码,其他代码省略,对函数组件完整加载逻辑感兴趣的可以查看《React18.2x源码解析:函数组件的加载过程》。
这里的Component
就是前面传入的render
函数【即child
函数】,而secondArg
参数就是传递的ref
对象:
这就是forwardRef
组件加载的逻辑,它自身并没有什么内容,它渲染的还是我们传入的render
函数【即Child
函数组件】,它的作用就只是包裹一层函数组件,在函数组件渲染的时候帮助传递ref
对象,我们就可以在函数组件中通过第二个参数拿到ref
进行使用。
js
function Child(props, ref) {
return (
<div className='Child'>
<div ref={ref}>DOM实例</div>
</div>
)
}
export default React.forwardRef(Child)
案例中,我们将ref
绑定到了div
上,后续的ref
更新和前面的DOM实例绑定逻辑完全一样,就不再重复讲述了。
useImperativeHandle
在实际的应用中,我们更多的会与useImperativeHandle
搭配使用,向父组件暴露一个对象,然后在父组件中就可以通过访问ref
对象来调用这些方法满足一些开发的需求。
js
import { imperativeHandleEffect } from 'react'
function Child(props, ref) {
// 搭配使用
useImperativeHandle(ref, () => {
return {
show: () => { console.log(ref) }
};
}, [])
return (
<div className='Child'>
<div>DOM实例</div>
</div>
)
}
export default React.forwardRef(Child)
下面我们就来看useImperativeHandle
是怎么绑定ref
的:
useImperativeHandle hook
的加载处理:
js
const HooksDispatcherOnMount: Dispatcher = {
useImperativeHandle: mountImperativeHandle,
}
查看mountImperativeHandle
方法:
js
function mountImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
// 依赖处理
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
const fiberFlags: Flags = UpdateEffect;
return mountEffectImpl(
fiberFlags,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
mountImperativeHandle
方法很简单,这里有一点要注意:useImperativeHandle hook
对应的副作用标记为UpdateEffect
,因为useImperativeHandle
和useEffect
加载都是调用的同一个方法mountEffectImpl
,它们的区别只有两个:
mountEffectImpl
第一个参数:副作用标记不同,因为不同的标记对应着不同的副作用逻辑,比如
js
// 副作用标记
useEffect: PassiveEffect
useLayoutEffect: LayoutEffect
useImperativeHandle: UpdateEffect
mountEffectImpl
第三个参数:回调函数不同,useEffect
的副作用回调是直接使用用户传入的create
,而useImperativeHandle
使用的是react内部定义的回调函数imperativeHandleEffect
,用户传入的create
只是作为它的参数。
这里mountEffectImpl
的作用主要有两点:
- 创建一个
hook
对象,存储useImperativeHandle hook
相关信息,并且与其他hook对象关联形成一个单向链表,这个链表会存储到函数组件Fiber
节点的memoizedState
属性之上。
- 创建一个
effect
对象,它的作用主要存储回调函数 以及触发,并且与其他effect关联形成一个单向环状链表,这个链表会存储到函数组件Fiber
节点的updateQueue
属性之上。
到这里,useImperativeHandle
的加载就执行完成了,imperativeHandleEffect
函数我们放到执行时再解析。
下面来到commit
阶段,触发useImperativeHandle
的回调,实现ref
对象的绑定。
js
function commitLayoutEffectOnFiber(
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
// 根据组件类型
switch (finishedWork.tag) {
case ForwardRef: {
// 传入的是layout相关的flag标记
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
)
来到Layout
子阶段,执行ForwardRef
组件对应的副作用操作。
注意: 前面我们已经知道useImperativeHandle hook
对应的副作用标记为UpdateEffect
,这里能够进入判断是因为LayoutMask
包含了这个副作用类型:
js
// Update as UpdateEffect
LayoutMask = Update | Callback | Ref | Visibility;
下面我们直接查看commitHookEffectListMount
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
// 当前函数组件的updateQueue属性,存储的是副作用链表
const updateQueue = finishedWork.updateQueue;
// 取出最后一个effect对象
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
// 获取第一个effect对象
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 开始循环处理
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
// 执行回调函数
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
前面我们已经说过,effect
链表存储在组件Fiber
节点的updateQueue
属性之上,这里就会取出这个副作用链表,因为我们这里只使用了一个存有副作用的hook
,所以链表中只存在一个effect
对象:
定义一个lastEffect
变量存储updateQueue.lastEffect
的内容,即最后一个effect
对象。
判断lastEffect
是否为null
,如果lastEffect
为null,代表当前组件没有使用过effect
相关的hook
。
当前肯定是有值的,继续向下执行。从lastEffect.next
中取出第一个effect
对象,开始按顺序循环处理副作用。
js
// 触发副作用回调
effect.create();
这里的create
就是之前useImperativeHandle
加载时设置的imperativeHandleEffect
方法:
这里我们就可以进入imperativeHandleEffect
方法,查看它绑定ref
的逻辑:
js
function imperativeHandleEffect<T>(
create: () => T,
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
) {
// ref为函数
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create();
refCallback(inst);
// 重置ref的内容
return () => {
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
// ref为对象的情况
const refObject = ref;
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}
这里绑定ref
的逻辑和之前类组件和DOM实例基本一致,也是区分为函数和对象两种。
js
useImperativeHandle(ref, () => {
return {
show: () => { console.log(ref) }
};
}, [])
当前我们的ref
为对象,所以就会进入else if
分支:
js
const inst = create();
refObject.current = inst;
直接调用回调函数create
,将返回的对象instance
赋值给ref.current
属性,完成ref
对象的绑定。
最后我们还是点击打印按钮,查看绑定完成的ref
对象:
结束语
以上就是react
中关于ref
对象的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!