【React源码笔记14】- ref全貌

React | 2020-08-25 22:31:18 453次 1次

ref 包括类组件和函数组件的写法:

类组件中的 createRef

函数组件中使用 useRef,其中还有 forwardRef、useImperativeHandle 等相关


一、类组件中

createRef 声明

// 这个使用时直接返回一个对象,然后肯定是在react-dom中赋值
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}


markRef 标记(render阶段)

beginWork中,对于类组件(ClassComponent)或者普通的 DOM 节点(HostComponent),会先执行 markRef,因为 React.creatElement 会将指定的 ref 属性解析完毕,初始值就是 createRef 函数返回的那个对象。

注意 shouldComponentUpdate 的返回值不会影响这个标记。

function markRef(current: Fiber | null, workInProgress: Fiber) {
  // 读取 ref 属性 :ref = {current: null}
  const ref = workInProgress.ref;
  // 初始 || 更新
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    // 这里仅仅是打上标记
    workInProgress.effectTag |= Ref;
  }
}

对于 HostComponentcompleteWork 中又进行一次创建标记(没有理解为什么执行这么多次)。


旧 ref 移除(commit阶段)

对于更新fiber 来说,需要清除下 current 中的 ref。

回顾:wip 是正在构建中的 fiber,如果是更新则有一个 alternate 相互指向,createWorkInProgress方法创建这个关系,判断是否可复用,否则就直接创建新节点

commitMutationEffects -- commitDetachRef

function commitMutationEffects(){
    ...
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      // 初始没有这个指向
      if (current !== null) {
        commitDetachRef(current);
      }
    }
    ...
}

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    // ref 传入一个方法 回调传null
    if (typeof currentRef === 'function') {
      currentRef(null);
    } else {
      // 就是将 current 置为空
      currentRef.current = null;
    }
  }
}

对于删除的节点,同样需要删除 ref

commitDeletion -> unmountHostComponents -> commitUnmount -> ClassComponent | HostComponent -> safelyDetachRef
// 同样的逻辑,在很多地方可以发现 react 中绝对不会考虑功能的复用,而是分工明确做事情
function safelyDetachRef(current: Fiber) {
  const ref = current.ref;
  if (ref !== null) {
    if (typeof ref === 'function') {
      ref(null);
    } else {
      ref.current = null;
    }
  }
}


ref 赋值(commit阶段)

上面步骤完事后,最后一步就是赋值,在 commitLayoutEffects 阶段将真实 dom 引用暴露出去:

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    // 获取真实 dom
    const instance = finishedWork.stateNode;
    ...
    if (typeof ref === 'function') {
      ref(instance );
    } else {
      ref.current = instance ;
    }
  }
}


二、函数组件中

useRef 声明

在函数组件中使用 ref 需要初始加载和更新两个阶段:

function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  // 参照之前的数据结构,ref挂载到 memoizedState
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  // 更新时直接返回初始创建时挂载的数据即可
  return hook.memoizedState;
}

接下来的流程和类组件中提到的保持一致


三、forwardRef

对于类组件,赋值 ref 属性,可以获取其实例,调用实例上的一些方法;

对于普通 DOM 节点,通过 ref 属性可以获取原生对象,进行节点操作;

对于函数组件,直接使用 ref 属性没有意义,并不可以直接获取函数组件中的某些方法;

但是可以通过 forwardRef 方法,传递 ref,能直接获取函数组件中指定的 dom,也可以配合 useImperativeHandle 方法取得函数组件内部的方法。另外,还可以进行高阶组件ref 转发。


类型创建:

export default function forwardRef(render) {
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}

普通一个组件的返回值如下:

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    ...
  };
  ...
  return element;
};

这样当使用 forwardRef 包裹之后,返回的类型就发生了改变:

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: {
        $$typeof: REACT_FORWARD_REF_TYPE,
        render,
    },
    ...
  };
  ...
  return element;
};

这样就会在 createFiberFromTypeAndProps 方法中创建对应的组件类型--ForwardRef

...
    switch (type) {
      case REACT_FRAGMENT_TYPE:
        ...
      case REACT_STRICT_MODE_TYPE:
        ...
      ...
      default: {
        if (typeof type === 'object' && type !== null) {
          switch (type.$$typeof) {
            ...
            case REACT_FORWARD_REF_TYPE:
              // 标记为 ForwardRef
              fiberTag = ForwardRef;
              break getTag;
            ...
          }
        }
      }
    }
...


执行:

创建完节点类型,再次进入 beginWork 时进入 ForwardRef 分支,执行 updateForwardRef

function updateForwardRef(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderExpirationTime: ExpirationTime,
) {
  // 传入的函数组件
  const render = Component.render;
  // 给函数组件身上挂载的 ref 属性
  const ref = workInProgress.ref;
  ...
  nextChildren = renderWithHooks(
      current,
      workInProgress,
      render,
      nextProps,
      ref,
      renderExpirationTime,
  );

  if (current !== null && !didReceiveUpdate) {
    //跳过更新
    ...
  }
  ...
  // 进入调和过程,和之前的内容对接起来了
  reconcileChildren(...);
  return workInProgress.child;
}

这里面核心的地方就是通过 renderWithHooks 方法调用,传入 ref 值,函数组件使用时第二个参数为转发的 ref,通常这个值也是 context

function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  secondArg: any,
  nextRenderExpirationTime: ExpirationTime,
){
    ...
    let children = Component(props, secondArg);
    ...
}

然而,到目前为止,我们仍然可以通过一些手段越过 react 中目前的限制,不使用 forwardRef 也可以让函数组件中的某些节点使用 ref,比如就当作一个普通属性传入进去,从数据流上看也没有什么问题。


四、useImperativeHandle

这个方法看似复杂,实则很简单,和 useEffect 类似,除了传入的回调函数不一致。

初始加载

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;
  // 和 useEffect不同的是这个方法 imperativeHandleEffect
  return mountEffectImpl(
    UpdateEffect,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

更新

function updateImperativeHandle<T>(...): void {
  ...
  return updateEffectImpl(
    ...
    imperativeHandleEffect.bind(null, create, ref),
    ...
  );
}

mountEffectImpl 和 updateEffectImpl 方法参考上一篇中 useEffect 部分 。

function imperativeHandleEffect<T>(
  create,
  ref
) {
  if (typeof ref === 'function') {
    ...
    // 都一个样 ,是函数则执行回调,传入参数。。
  } else if (ref !== null && ref !== undefined) {
    const refObject = ref;
    // useImperativeHandle第二个参数是一个函数,返回一个对象结构
    const inst = create();
    // 直接挂载到了 current  简单粗暴
    refObject.current = inst;
    return () => {
      refObject.current = null;
    };
  }
}

到这里和 ref 相关的东西就齐全了,对于 forwardRef 和 useImperativeHandle 这两个 api 的设计,如果单纯的从开发角度,是完全没有必要的,但是到这里再看这两个方法以及使用,很明显的感觉到 react 在极力的推动我们走向函数式编程,尽管 ref 原本可以灵活的使用。

比如我们在一个类组件中直接取 this.props.ref 时,会提示错误,在 defineRefPropWarningGetter 方法中做出了限制:

function defineRefPropWarningGetter(props, displayName) {
  const warnAboutAccessingRef = function() {
    if (__DEV__) {
      if (!specialPropRefWarningShown) {
        specialPropRefWarningShown = true;
        console.error(
          '%s: `ref` is not a prop. Trying to access it will result ' +
            'in `undefined` being returned. If you need to access the same ' +
            'value within the child component, you should pass it as a different ' +
            'prop. (https://fb.me/react-special-props)',
          displayName,
        );
      }
    }
  };
  warnAboutAccessingRef.isReactWarning = true;
  // 属性拦截
  Object.defineProperty(props, 'ref', {
    get: warnAboutAccessingRef,
    configurable: true,
  });
}

1人赞

分享到: