react框架ref系列API的用法及原理解析

使用过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
  • 普通DOMFiber节点的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节点的创建处理都分为beginWorkcompleteWork两个工作流程。

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,因为useImperativeHandleuseEffect加载都是调用的同一个方法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对象的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!

相关推荐
周亚鑫6 分钟前
vue3 pdf base64转成文件流打开
前端·javascript·pdf
落魄小二15 分钟前
el-table 表格索引不展示问题
javascript·vue.js·elementui
y52364816 分钟前
Javascript监控元素样式变化
开发语言·javascript·ecmascript
Justinc.22 分钟前
CSS3新增边框属性(五)
前端·css·css3
fruge30 分钟前
纯css制作声波扩散动画、js+css3波纹催眠动画特效、【css3动画】圆波扩散效果、雷达光波效果完整代码
javascript·css·css3
neter.asia38 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫39 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
嚣张农民1 小时前
JavaScript中Promise分别有哪些函数?
前端·javascript·面试
光影少年1 小时前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_1 小时前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习