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

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

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

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

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

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

useRef

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

  1. function mountRef<T>(initialValue: T): {|current: T|} {
  2. // 获取当前useRef hook
  3. const hook = mountWorkInProgressHook();
  4. // 创建ref
  5. const ref = {current: initialValue};
  6. hook.memoizedState = ref;
  7. return ref;
  8. }
  9. function updateRef<T>(initialValue: T): {|current: T|} {
  10. // 获取当前useRef hook
  11. const hook = updateWorkInProgressHook();
  12. // 返回保存的数据
  13. return hook.memoizedState;
  14. }

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

可见,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的工作流程。

ref的工作流程

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

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

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

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

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

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

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

所以,对应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;
  6. // ...

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

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

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

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

render阶段

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

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

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

beginWork中,如下两处调用了markRef

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

completeWork中,如下两处调用了markRef

ScopeComponent是一种用于管理focus的测试特性,详见PRuseRef - 图11 (opens new window)

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

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

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

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

commit阶段

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

  1. function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  2. while (nextEffect !== null) {
  3. const effectTag = nextEffect.effectTag;
  4. // ...
  5. if (effectTag & Ref) {
  6. const current = nextEffect.alternate;
  7. if (current !== null) {
  8. // 移除之前的ref
  9. commitDetachRef(current);
  10. }
  11. }
  12. // ...
  13. }
  14. // ...

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

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

接下来,在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. }

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

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

commitLayoutEffect会执行commitAttachRef(赋值ref

  1. function commitAttachRef(finishedWork: Fiber) {
  2. const ref = finishedWork.ref;
  3. if (ref !== null) {
  4. // 获取ref属性对应的Component实例
  5. const instance = finishedWork.stateNode;
  6. let instanceToUse;
  7. switch (finishedWork.tag) {
  8. case HostComponent:
  9. instanceToUse = getPublicInstance(instance);
  10. break;
  11. default:
  12. instanceToUse = instance;
  13. }
  14. // 赋值ref
  15. if (typeof ref === 'function') {
  16. ref(instanceToUse);
  17. } else {
  18. ref.current = instanceToUse;
  19. }
  20. }
  21. }

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

总结

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

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

  • 对于赋值了ref属性的HostComponentClassComponent,会在render阶段经历赋值Ref effectTag,在commit阶段执行对应ref操作。