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

在我们讲到:

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

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

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

你可以在看到这段代码

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

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

  1. export function createRef(): RefObject {
  2. const refObject = {
  3. current: null,
  4. };
  5. return refObject;
  6. }

你可以从这里useRef - 图2 (opens new window)看到这段代码

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

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

  1. // HostComponent
  2. <div ref={domRef}></div>
  3. // ClassComponent / ForwardRef
  4. <App ref={cpnRef} />

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

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

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

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

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

  1. // ...
  2. export const Placement = /* */ 0b0000000000000010;
  3. export const Update = /* */ 0b0000000000000100;
  4. export const Deletion = /* */ 0b0000000000001000;
  5. export const Ref = /* */ 0b0000000010000000;

你可以在ReactSideEffectTags文件 (opens new window)中看到ref对应的effectTag

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

  • render阶段为含有ref属性的fiber添加Ref effectTag

  • commit阶段为包含Ref effectTagfiber执行对应操作

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

  1. // beginWork的markRef
  2. const ref = workInProgress.ref;
  3. if (
  4. (current === null && ref !== null) ||
  5. (current !== null && current.ref !== ref)
  6. ) {
  7. // Schedule a Ref effect
  8. workInProgress.effectTag |= Ref;
  9. }
  10. }
  11. // completeWork的markRef
  12. function markRef(workInProgress: Fiber) {
  13. workInProgress.effectTag |= Ref;
  14. }

你可以在这里useRef - 图5 (opens new window)看到beginWorkmarkRef、看到completeWorkmarkRef

beginWork中,如下两处调用了markRef

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

  • ,对应HostComponent

completeWork中,如下两处调用了markRef

  • fiber类型为HostComponentClassComponentScopeComponent(这种情况我们不讨论)

  • 对于mountworkInProgress.ref !== null,即存在ref属性

  • 对于updatecurrent.ref !== workInProgress.ref,即属性改变

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

你可以在看到这段代码

  1. function commitDetachRef(current: Fiber) {
  2. const currentRef = current.ref;
  3. if (currentRef !== null) {
  4. // function类型ref,调用他,传参为null
  5. currentRef(null);
  6. } else {
  7. // 对象类型ref,current赋值为null
  8. currentRef.current = null;
  9. }
  10. }
  11. }

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

mutation阶段一节我们讲到

对于Deletion effectTagfiber,会执行commitDeletion

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

  1. function safelyDetachRef(current: Fiber) {
  2. const ref = current.ref;
  3. if (ref !== null) {
  4. if (typeof ref === 'function') {
  5. try {
  6. ref(null);
  7. } catch (refError) {
  8. captureCommitPhaseError(current, refError);
  9. }
  10. } else {
  11. ref.current = null;
  12. }
  13. }
  14. }

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

commitLayoutEffect会执行commitAttachRef(赋值ref

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

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

  • 对于FunctionComponentuseRef负责创建并返回对应的ref