useRef

ref是reference(引用)的缩写。在React中,我们习惯用ref保存DOM。

事实上,任何需要被"引用"的数据都可以保存在ref中,useRef的出现将这种思想进一步发扬光大。

在Hooks数据结构一节我们讲到:

对于useRef(1),memoizedState保存{current: 1}

本节我们会介绍useRef的实现,以及ref的工作流程。

由于string类型的ref已不推荐使用,所以本节针对function | {current: any}类型的ref。

useRef

与其他Hook一样,对于mountupdateuseRef对应两个不同dispatcher

function mountRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = mountWorkInProgressHook();
  // 创建ref
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}
function updateRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = updateWorkInProgressHook();
  // 返回保存的数据
  return hook.memoizedState;
}

你可以在这里看到这段代码

可见,useRef仅仅是返回一个包含current属性的对象。

为了验证这个观点,我们再看下React.createRef方法的实现:

export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}

你可以从这里看到这段代码

了解了ref的数据结构后,我们再来看看ref的工作流程。

ref的工作流程

React中,HostComponentClassComponentForwardRef可以赋值ref属性。

// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />

其中,ForwardRef只是将ref作为第二个参数传递下去,不会进入ref的工作流程。

所以接下来讨论ref的工作流程时会排除ForwardRef

// 对于ForwardRef,secondArg为传递下去的ref
let children = Component(props, secondArg);

你可以在这里看到这段代码

我们知道HostComponentcommit阶段mutation阶段执行DOM操作。

所以,对应ref的更新也是发生在mutation阶段

再进一步,mutation阶段执行DOM操作的依据为effectTag

所以,对于HostComponentClassComponent如果包含ref操作,那么也会赋值相应的effectTag

// ...
export const Placement = /*                    */ 0b0000000000000010;
export const Update = /*                       */ 0b0000000000000100;
export const Deletion = /*                     */ 0b0000000000001000;
export const Ref = /*                          */ 0b0000000010000000;
// ...

你可以在ReactSideEffectTags文件中看到ref对应的effectTag

所以,ref的工作流程可以分为两部分:

  • render阶段为含有ref属性的fiber添加Ref effectTag
  • commit阶段为包含Ref effectTagfiber执行对应操作

render阶段

render阶段beginWorkcompleteWork中有个同名方法markRef用于为含有ref属性的fiber增加Ref effectTag

// beginWork的markRef
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.effectTag |= Ref;
  }
}
// completeWork的markRef
function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}

你可以在这里看到beginWorkmarkRef这里看到completeWorkmarkRef

beginWork中,如下两处调用了markRef

注意ClassComponent即使shouldComponentUpdatefalse该组件也会调用markRef

completeWork中,如下两处调用了markRef

ScopeComponent是一种用于管理focus的测试特性,详见PR

总结下组件对应fiber被赋值Ref effectTag需要满足的条件:

  • fiber类型为HostComponentClassComponentScopeComponent(这种情况我们不讨论)
  • 对于mountworkInProgress.ref !== null,即存在ref属性
  • 对于updatecurrent.ref !== workInProgress.ref,即ref属性改变

commit阶段

commit阶段mutation阶段中,对于ref属性改变的情况,需要先移除之前的ref

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;
    // ...
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        // 移除之前的ref
        commitDetachRef(current);
      }
    }
    // ...
  }
  // ...

你可以在这里看到这段代码

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') {
      // function类型ref,调用他,传参为null
      currentRef(null);
    } else {
      // 对象类型ref,current赋值为null
      currentRef.current = null;
    }
  }
}

接下来,在mutation阶段,对于Deletion effectTagfiber(对应需要删除的DOM节点),需要递归他的子树,对子孙fiberref执行类似commitDetachRef的操作。

在mutation阶段一节我们讲到

对于Deletion effectTagfiber,会执行commitDeletion

commitDeletion——unmountHostComponents——commitUnmount——ClassComponent | HostComponent类型case中调用的safelyDetachRef方法负责执行类似commitDetachRef的操作。

function safelyDetachRef(current: Fiber) {
  const ref = current.ref;
  if (ref !== null) {
    if (typeof ref === 'function') {
      try {
        ref(null);
      } catch (refError) {
        captureCommitPhaseError(current, refError);
      }
    } else {
      ref.current = null;
    }
  }
}

你可以在这里看到这段代码

接下来进入ref的赋值阶段。我们在Layout阶段一节讲到

commitLayoutEffect会执行commitAttachRef(赋值ref

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    // 获取ref属性对应的Component实例
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    // 赋值ref
    if (typeof ref === 'function') {
      ref(instanceToUse);
    } else {
      ref.current = instanceToUse;
    }
  }
}

至此,ref的工作流程完毕。

总结

本节我们学习了ref的工作流程。

  • 对于FunctionComponentuseRef负责创建并返回对应的ref
  • 对于赋值了ref属性的HostComponentClassComponent,会在render阶段经历赋值Ref effectTag,在commit阶段执行对应ref操作。
下一节:在了解其他hook的实现后,理解useMemo与useCallback的实现非常容易。

本节我们以mount与update两种情况分别讨论这两个hook。