Skip to content

Ref

对于 React 中 ref 的作用,按照其数据来源可以区分为三大类: 1. 访问 DOM; 2. 访问组件。 3. 访问实时数据。

按照其使用方法又可以为两大类 : 1. Class 组件 2. 函数式组件

  • 其中 Class 组件中借助于 this.ref = createRef(null) 去申明 ref 的对象, 通过 <input ref={this.ref}> ,<ClassComponent ref={(instance) => this.ref.current= instance }> 等方式去进行赋值

  • 对于函数式组件,确需要借助于特定的两个方法去实现 1. React.forwardRef(() => {}) 去将 ref 与函数式组件关联 , 2. React.useImperativeHandle(ref , () => { }) 去将 ref 与 其具体暴露的方法进行关联。

1. Ref 的理解

Ref 的理解可以可以分为以下三个部分:

  1. Ref 的创建流程,如通过 React.createRef 或者 React.useRef 来创建一个 ref 对象。
  2. Ref 的赋值流程,如通过 ref={xxx} 来给 ref 对象赋值。
  3. Ref 的使用流程,如通过 ref.current 来获取 ref 对象的值。

下面我们就按照这三个部分来详细介绍 Ref 的使用。

2. Ref 的创建流程

Ref 的创建流程主要分为以下两个部分:

  1. 函数式组件中 创建 Ref 对象。
  2. 类组件中 创建 Ref 对象。

在这两个组件中对于 Ref 的创建流程是不一样的。

2.1. createRef 创建流程

Class 组件中,我们会通过 createRef 去创建一个 Ref 对象,其会被保存在类组件实例上,它的实现很简单。

如下:

js
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}

2.2. useRef 的创建流程

对于函数式组件中,我们会通过 useRef 去创建一个 Ref 对象,它的实现也很简单。但是还是按照 Hook 的通用流程来分析

2.2.1. 初次渲染阶段

useRef 在初次渲染阶段实现的逻辑和 createRef 是一样的,主要是生成一个 {current: initialValue}类型的对象,其存放地址不一样,函数式组件存放到 hook.memoizedState

js
/**
 * useRef 在初次渲染阶段实现
 *  主要是生成一个 {current: initialValue} 存放到 hook.memoizedState
 * @param {*} initialValue
 */
function mountRef<T>(initialValue: T): {| current: T |} {
  // 创建一个 hook 对象
  const hook = mountWorkInProgressHook();
  if (enableUseRefAccessWarning) {
  } else {
    // 创建一个 { current: initialValue } 对象
    const ref = { current: initialValue };
    // 并将其赋值给 hook.memoizedState
    hook.memoizedState = ref;
    return ref;
  }
}

2.2.2. 更新渲染阶段

useRef 在更新渲染阶段实现的逻辑更简单,就是去取存放在函数式组件 FiberNode 对象上的 hook.memoizedState中的数据,即初次渲染阶段存放的{current: initialValue}类型的对象。

js
function updateRef<T>(initialValue: T): {| current: T |} {
  // 复用初次渲染阶段生成的 hook
  const hook = updateWorkInProgressHook();
  // 直接返回 hook.memoizedState
  return hook.memoizedState;
}

3. Ref 的赋值流程

Ref 的赋值流程主也要也在两种组件类型中(函数式组件 和 类组件),但是其值可以分为以下几种类型

  1. 自定义类型的 Ref 对象
  2. 绑定 DOM 元素
  3. 绑定函数式组件
  4. 绑定类组件

下面我们分别对这四种类型的 Ref 对象进行分析。

3.1. 自定义类型的 Ref 对象

自定义类型的 Ref 对象就是通过 React.createRef 或者 React.useRef 来创建的 Ref 对象,其值为 {current: null} 类型的对象。然后通过 this.ref.current = xxx 来给 Ref 对象赋值。

其特点就是 值是实时更新的,不像 state 类型的数据是异步更新的。

3.2. 绑定 DOM 元素

绑定 DOM 元素类型的 Ref 对象就是直接将 DOM 元素赋值给 Ref 对象。

js
this.inputRef = React.createRef(); // 类组件
<input ref={this.inputRef} />; // 就可以使用 this.inputRef.current 来获取 DOM 元素
const inputRef = React.useRef(); // 函数组件
<input ref={inputRef} />; // 就可以使用 inputRef.current 来获取 DOM 元素

其核心在 HostComponent 组件的 render 和 commit 阶段。在 render 阶段的 compeleteWork 流程中会根据是否可以复用 和 是否需要删除等需要更新状态去创建一个 Ref 类型的副作用添加到 FiberNode.flags 中, 在 commit 阶段根据 flags & Ref知道存在重新赋值的 ref,那么就通过 safelyDetachRef()去更新 ref 的值

所以其涉及到两个阶段:

1. compeleteWork(render 阶段)

js
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  switch (workInProgress.tag) {
    case HostComponent: {
      // 元素节点可以复用的流程
      // 1. DOM 元素节点复用流程
      if (current !== null && workInProgress.stateNode != null) {
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        // 2. DOM 元素节点创建流程
        if (workInProgress.ref !== null) {
          markRef(workInProgress);
        }
      }
      return null;
    }
  }
}

通过 current !== null && workInProgress.stateNode != null 来判断 原生节点是进入复用流程还是创建流程。

  • 对于复用流程,其可能只是 DOM 元素的属性更新,也有可能是 DOM 元素的子元素发生了变化。
  • 对于创建流程,其就是 DOM 元素的创建流程。

其实还缺少了一个 DOM 元素的删除流程(ref.current = DOM 转变成 ref.current = null ),对于这种情况不需要标记存在 Ref 副作用,而是直接根据 Deletion 类型的副作用在 commitDeletionEffectsOnFiber 中去处理。 具体是在 commitMutationEffects 的 处理 parentFiber.deletions的流程中,

2. Ref 的解绑 (commitMutationEffects)

在 render 阶段主要是标记 更新、创建 状态的 DOM 元素存在 Ref 类型的副作用,在 commitMutationEffects 阶段主要是去执行 Ref 类型的副作用的旧节点的 ref 解绑工作。

具体如下

  • 创建、更新类型的 Ref 副作用
js
// 如果存在 ref 属性,那就执行更新
if (flags & Ref) {
  if (current !== null) {
    // 存在旧节点
    // 执行旧节点的 ref 解绑
    safelyDetachRef(current, current.return);
  }
}
  • 删除类型的 Ref 副作用

刚才说对于删除类型的 DOM 元素在 render 阶段没有处理,但是其 DOM 元素本身存在 Deletion 类型的副作用,所以在 commit 阶段也会去处理。

具体如下

js
function commitDeletionEffectsOnFiber(
  // 根节点
  finishedRoot: FiberRoot,
  // 父节点
  nearestMountedAncestor: Fiber,
  // 待删除的节点
  deletedFiber: Fiber
) {
  switch (deletedFiber.tag) {
    // 元素节点类型的 处理 ref
    case HostComponent: {
      if (!offscreenSubtreeWasHidden) {
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
      }
    }
  }
}

所以对于 Ref 类型的副作用处理,都是通过 safelyDetachRef 来处理的。 其实现具体如下

js
/**
 * 执行定义了ref的待删除DOM节点的ref回调
 *  - 如果是函数类型,执行回调函数 ref(null)
 *  - 如果是 useRef 类型,将 ref.current 赋值为 null
 *
 * @param {*} current
 * @param {*} nearestMountedAncestor
 */
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
  // 获取 ref 属性
  const ref = current.ref;
  if (ref !== null) {
    // 如果是函数类型,执行函数
    if (typeof ref === "function") {
      let retVal;
      try {
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          current.mode & ProfileMode
        ) {
          try {
            startLayoutEffectTimer();
            retVal = ref(null);
          } finally {
            recordLayoutEffectDuration(current);
          }
        } else {
          retVal = ref(null);
        }
      } catch (error) {
        captureCommitPhaseError(current, nearestMountedAncestor, error);
      }
    } else {
      // 不是函数类型的 将其赋值为 null
      ref.current = null;
    }
  }
}

3. Ref 的绑定(commitLayoutEffects)

js
/**
 * 将最终的 fiber 数有关的 ref 提交给 ref.current 或者 ref(instanceToUse)
 * @param {*} finishedWork
 */
function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    // 获取 stateNode
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      // 元素类型的 那就是 DOM 元素
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    // Moved outside to ensure DCE works with this flag
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    // 2. 根据 ref 值的类型,执行 ref(instanceToUse) 或者  ref.current = instanceToUse
    if (typeof ref === "function") {
      let retVal;
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        finishedWork.mode & ProfileMode
      ) {
        try {
          startLayoutEffectTimer();
          retVal = ref(instanceToUse);
        } finally {
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
        retVal = ref(instanceToUse);
      }
    } else {
      ref.current = instanceToUse;
    }
  }
}

从绑定 Ref 的流程看出,其不是仅仅只作用于 DOM 类型元素的绑定工作,其对于函数式组件、类组件等类型的元素也会去绑定 Ref。只是其绑定的值不同

  • 对于 DOM 元素类型的,其绑定的值 通过 getPublicInstance(instance) 来获取的 DOM 元素的值。
  • 对于函数式组件、类组件等类型的,其绑定的值 就是 instanceFiberNode.stateNode

3.3. 绑定 Class 组件

对于类组件的绑定 Ref 流程,其主要是在 commitLayoutEffects 中去处理的。

1. render 阶段

js
function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes
) {
  markRef(current, workInProgress);
}

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;
    if (enableSuspenseLayoutEffectSemantics) {
      workInProgress.flags |= RefStatic;
    }
  }
}

可见对于 类组件 Ref 的流程中也是在 beginWork 阶段去标记存在 Ref 副作用的。

2. Ref 的解绑 (commitMutationEffects)

在 render 阶段主要是标记 更新、创建 状态的 DOM 元素存在 Ref 类型的副作用,在 commitMutationEffects 阶段主要是去执行 Ref 类型的副作用的旧节点的 ref 解绑工作。

具体如下

  • 创建、更新类型的 Ref 副作用
js
// 如果存在 ref 属性,那就执行更新
if (flags & Ref) {
  if (current !== null) {
    // 存在旧节点
    // 执行旧节点的 ref 解绑
    safelyDetachRef(current, current.return);
  }
}
  • 删除类型的 Ref 副作用

刚才说对于删除类型的 DOM 元素在 render 阶段没有处理,但是其 DOM 元素本身存在 Deletion 类型的副作用,所以在 commit 阶段也会去处理。

具体如下

js
function commitDeletionEffectsOnFiber(
  // 根节点
  finishedRoot: FiberRoot,
  // 父节点
  nearestMountedAncestor: Fiber,
  // 待删除的节点
  deletedFiber: Fiber
) {
  switch (deletedFiber.tag) {
    case ClassComponent: {
      // Ref 的 解绑
      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      return;
    }
  }
}

所以对于 Ref 类型的副作用处理,都是通过 safelyDetachRef 来处理的。

3. Ref 的绑定(commitLayoutEffects)

具体看 DOM 元素的绑定流程

3.4. 绑定 函数组件

对于在函数式组件上绑定 Ref 的流程,其不像类组件那样存在一个 实例对象 instace , 所以也无法直接访问函数式组件中的 实例方法,所以对于函数式组件的 Ref 绑定流程,其需要借助于一个单独的函数 forwardRef(FunctionalComponent , ref ) 去完成 ref 的绑定工作

下面我们看一下这个方法 forwardRef 的实现

forwardRef

例子

jsx
const FunctionalRefComponent = React.forwardRef((props, ref) => {
  // 使用 ref 来绑定 DOM 元素
  React.useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));
  return (
    <div>
      <input ref={inputRef} />
    </div>
  );
});

其主要分为以下几个阶段的处理工作

执行阶段

js
/**
 * React.forwardRef 接收一个 render 函数作为参数,返回一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象
 *   重点: 创建一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象
 * @param {*} render
 * @returns
 */
export function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node
) {
  if (__DEV__) {
    if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
      console.error(
        "forwardRef requires a render function but received a `memo` " +
          "component. Instead of forwardRef(memo(...)), use " +
          "memo(forwardRef(...))."
      );
    } else if (typeof render !== "function") {
      console.error(
        "forwardRef requires a render function but was given %s.",
        render === null ? "null" : typeof render
      );
    } else {
      if (render.length !== 0 && render.length !== 2) {
        console.error(
          "forwardRef render functions accept exactly two parameters: props and ref. %s",
          render.length === 1
            ? "Did you forget to use the ref parameter?"
            : "Any additional parameter will be undefined."
        );
      }
    }

    if (render != null) {
      if (render.defaultProps != null || render.propTypes != null) {
        console.error(
          "forwardRef render functions do not support propTypes or defaultProps. " +
            "Did you accidentally pass a React component?"
        );
      }
    }
  }
  //  forwardRef 返回的是一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象
  //  该对象的 render 属性指向传入的 render 函数
  const elementType = {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
  if (__DEV__) {
    let ownName;
    Object.defineProperty(elementType, "displayName", {
      enumerable: false,
      configurable: true,
      get: function () {
        return ownName;
      },
      set: function (name) {
        ownName = name;
        if (!render.name && !render.displayName) {
          render.displayName = name;
        }
      },
    });
  }
  return elementType;
}

可以看出对于 forwardRef 方法,其主要是创建一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象,该对象的 render 属性指向传入的 render 函数。

同时看其 dev 环境下的一些错误处理。可以看出我们使用 forwardRef 方法时,其传入的函数必须

    1. 不是一个 React.memo()包裹的组件
    1. 必须是一个函数,且通过 render.length 来判断其参数存在的情况下必须要 2 个入参,即 propsref
    1. 函数不能存在 propTypesdefaultProps 不能是 Class 组件类型的

同时通过 Object.defineProperty 来给 REACT_FORWARD_REF_TYPE 类型的内置组件对象添加 displayName 属性。并将其直接指向 render 函数。

beginWork 阶段

forwardRef 函数创建的组件类型为 REACT_FORWARD_REF_TYPE, 然后通过 createFiberFromTypeAndProps创建 Fiber 对象的时候其组件类型转换成 ForwordRef 类型,那么在 beginWork 阶段对于 ForwordRef 类型 的 处理流程跟 FunctionalComponent 组件的流程基本差不多。

其主要区别是什么?

  1. 函数式组件的函数不一样
  • FuntionalComponent 或者 IndeterminateComponent 类型的 render 是直接通过 createFiberFromTypeAndProps 获取的
  • ForwardRef组件的 render 是直接通过 Component.render 获取的
  1. 函数组件的 ref 获取不一样
  • FuntionalComponent 或者 IndeterminateComponent 类型的 ref 是直接通过 createFiberFromTypeAndProps 获取的
  • ForwardRef组件的 render 是直接通过 workInProgress.ref 获取的
js
function updateForwardRef(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes
) {
  const render = Component.render;
  const ref = workInProgress.ref;

  nextChildren = renderWithHooks(
    current,
    workInProgress,
    render, // 传入的render
    nextProps,
    ref, // 传入的 ref
    renderLanes
  );

  return workInProgress.child;
}

那么怎么去给函数式组件的 ref 提供实例对象呐? 这就靠 forwradRef一起使用的另外一个方法 useImperativeHandle 其本身是一个 钩子函数

useImperativeHandle

初次渲染阶段

js
function mountImperativeHandle<T>(
  ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null
): void {
  // 开发状态 检查第二参数必须是函数类型,提供 ref 绑定的实例函数数据
  // 生成 deps 依赖更新的对象数组
  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;

  return mountEffectImpl(
    fiberFlags,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps
  );
}

useImperativeHandle 初次渲染的 执行函数,其主要是通过 useEffect 去维护 ref提供数据的绑定关系的,所以其也包含 3 个关键数据

  1. 副作用 deps : 即 [ref , ...deps] ref 和自定义依赖
  2. 处理函数 create : imperativeHandleEffect
  3. 销毁回调函数: imperativeHandleEffect 提供的 refCallback(null)refCallback.current = null

所以其核心就是 imperativeHandleEffect方法

imperativeHandleEffect

js
function imperativeHandleEffect<T>(
  create: () => T,
  ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void
) {
  // function 类型的 ref
  if (typeof ref === "function") {
    const refCallback = ref;
    // 生成 函数式组件提供方法对象
    const inst = create();
    // 回调 ref()
    refCallback(inst);
    return () => {
      refCallback(null);
    };
  } else if (ref !== null && ref !== undefined) {
    const refObject = ref;

    const inst = create();
    refObject.current = inst;
    return () => {
      refObject.current = null;
    };
  }
}

按照 useEffect 的处理函数的方式去创建 函数式组件 ref 和 函数式组件提供方法对象 的绑定流程

  1. 按照 ref 是 函数 和 ref 对象 类型两种方式去将 create()的结果绑定到 ref.current 的结果上
  2. 提供对应的 effect 副作用 destroy 的销毁函数 如: refCallback(null) 或 refObject.current = null;

更新渲染阶段

js
function updateImperativeHandle<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;

  return updateEffectImpl(
    UpdateEffect,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps
  );
}

更新渲染阶段处理, 没有什么特殊的逻辑,其跟初次渲染差不多,还是借助于 useEffect 更新阶段的实现方法 updateEffectImpl 绑定处理函数 imperativeHandleEffect

4. 总结

通过上面源码分析可以得出 Ref 主要在 3 个流程中进行处理

  • completeWork 阶段: 收集 Ref 类型的副作用
  • commitMutationEffects 阶段 : 将存在 Ref 副作用的与旧节点进行解绑
  • commitLayoutEffects 阶段 : 将存在 Ref 副作用的与新节点进行绑定

当然对于特殊的函数式组件,因为其不存在生命周期对象,也不存在什么实例对象的概念, 所以其 ref 的处理需要通过 forwordRefuseImperativeHandle 两个方法进行处理

  • 对于forwordRef:

其创建了一个新的内置组件类型 REACT_FORWARD_REF_TYPE的组件,从而可以访问组件属性上的 ref 属性,然后通过 FunctionalComponent的流程去处理传入的第一个参数(函数式组件),这样就将 ref 的绑定与函数解耦。

  • 对于 useImperativeHandle

其主要借助于 useEffect的实现方式去维护和管理 ref其create()数据 的关系