React.forwardRef的應用場景及源碼解析

1、React.forwardRef的應用場景
ref 的做用是獲取實例,多是 DOM 實例,也多是 ClassComponent 的實例。javascript

但會碰到如下問題:
① 若是目標組件是一個 FunctionComponent 的話,是沒有實例的(PureComponent),此時用 ref 去傳遞會報錯:html

② 若是你是一個庫的開發者的話,使用該庫的人是不知道庫的組件類別的,那麼當庫組件類別是 FunctionComponent 的時候,使用者想用 ref 獲取庫組件,怎麼辦?java

③ redux 中的 connect 方法將組件包裝成高階組件(HOC),那麼我如何經過 ref 去獲取包裝前的組件實例?react

④ props 不能傳遞 refweb

React 官方也表述了 ref 的使用條件:redux

React.forwardRef存在的意義就是爲了解決以上問題。api

關於React.forwardRef的使用,請參考:bash

zh-hans.reactjs.org/docs/react-…app

接下來咱們講下React.forwardRef的源碼ide

2、React.forwardRef
做用:
建立一個 React 組件,該組件可以將其接收的 ref 屬性轉發到內部的一個組件中

源碼:

export default function forwardRef<PropsElementTypeReact$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>
) => React$Node,
{
  //刪除了 dev 代碼

  return {
    //被forwardRef包裹後,組件內部的$$typeof是REACT_FORWARD_REF_TYPE
    $$typeof: REACT_FORWARD_REF_TYPE,
    //render即包裝的FunctionComponent,ClassComponent是不用forwardRef的
    render,
  };
}
複製代碼

解析:
(1) 返回的是一個對象,即下面的變量 Child:

const Child = React.forwardRef((props, ref) => (
  <button ref={ref}>
    {props.children}
  </button>

));

console.log(Child,'Child29')
複製代碼

(2) 返回的對象裏面的$$typeof,並不說明Child組件類型是REACT_FORWARD_REF_TYPE

我以前寫的 React源碼解析之React.createElement()和ReactElement() 中有提到ReactElementtypeREACT_ELEMENT_TYPE

const ReactElement = function(type, key, ref, self, source, owner, props{
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    //標識element的類型
    //由於jsx都是經過createElement建立的,因此ReactElement的類型固定:爲REACT_ELEMENT_TYPE
    //重要!由於react最終渲染到DOM上時,須要判斷$$typeof===REACT_ELEMENT_TYPE
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    //設置元素的內置屬性
    type: type,

  };
}
複製代碼

注意:
① 變量Child是對象:

{
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
複製代碼

Child組件是 ReactElement 對象,二者不同

  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    typetype,
  };
複製代碼

當把Child轉變爲 ReactElement 的時候,Child對象做爲Child組件的type屬性:

  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    //注意!!!
    type: {
      $$typeof: REACT_FORWARD_REF_TYPE,
      render,
    },
  };
複製代碼

不要認爲被forwardRef包裹後,React 組件的$$typeof的值會改變成REACT_FORWARD_REF_TYPE

② 只有 FunctionComponent 才用獲得forwardRef,ClassComponent 不須要

3、updateForwardRef
做用:
更新ref指向及被React.forwardRef包裹的FunctionComponent

源碼:

//更新被React.forwardRef包裹的 FunctionComponent
function updateForwardRef(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderExpirationTime: ExpirationTime,
{
  // TODO: current can be non-null here even if the component
  // hasn't yet mounted. This happens after the first render suspends.
  // We'll need to figure out if this is fine or can cause issues.

  //刪除了 dev 代碼

  //Component:{
  //   $$typeof: REACT_FORWARD_REF_TYPE,
  //   render,
  // }

  //FunctionComponent
  const render = Component.render;
  // 開發層面上不容許FunctionComponent,但你打印 props 的話是有的,
  // 由於是 React 只容許內部經過 props 傳進來 ref
  const ref = workInProgress.ref;

  // The rest is a fork of updateFunctionComponent
  let nextChildren;
  //context 相關的可跳過
  prepareToReadContext(workInProgress, renderExpirationTime);
  prepareToReadEventComponents(workInProgress);
  if (__DEV__) {
    //刪除了 dev 代碼
  } else {
    //渲染的過程當中,對裏面用到的 hook函數作一些操做
    //關於renderWithHooks的講解,請看:https://www.jianshu.com/p/959498695e83

    //注意:在updateFunctionComponent()中傳的參數不是 ref,
    //而是 context:nextChildren = renderWithHooks(
    //   current,
    //   workInProgress,
    //   Component,
    //   nextProps,
    //   傳的是 context 而不是 ref
    //   context,
    //   renderExpirationTime,
    // );
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      render,
      nextProps,
      ref,
      renderExpirationTime,
    );
    //renderWithHooks 內部經過let children = Component(props, refOrContext)來更新 ref 或 context
  }

  //若是 props 相同,而且 ref 也相同的話,就不須要更新
  if (current !== null && !didReceiveUpdate) {
    //跳過hooks更新
    //關於bailoutHooks的講解,請看:https://www.jianshu.com/p/959498695e83
    bailoutHooks(current, workInProgress, renderExpirationTime);
    //跳過該節點及全部子節點的更新
    //關於bailoutOnAlreadyFinishedWork的講解,請看:https://www.jianshu.com/p/06b18db8b5d4
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }

  // React DevTools reads this flag.
  workInProgress.effectTag |= PerformedWork;
  //將 ReactElement 變成 fiber對象,並更新,生成對應 DOM 的實例,並掛載到真正的 DOM 節點上
  //關於reconcileChildren的講解,請看:https://www.jianshu.com/p/959498695e83
  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}
複製代碼

解析:
(1) 邏輯比較簡單,裏面大部分函數在以前的文章裏已解析過:
① 關於 renderWithHooksbailoutHooksreconcileChildren 的講解,請看:
React源碼解析之FunctionComponent(上)

② 關於 bailoutOnAlreadyFinishedWork 的講解,請看:
React源碼解析之workLoop

(2) ref 的更新是在renderWithHooks中:

let children = Component(props, refOrContext);
複製代碼

這裏的Component2、React.forwardRef中 Child 對象的render屬性,也就是執行渲染FunctionComponent的方法

refOrContext在這裏是workInProgress.ref

也就是說Component(props, refOrContext)的參數即React.forwardRef中的參數(props, ref)

React.forwardRef((props, ref) => (
  xxx
));
複製代碼

(完)

相關文章
相關標籤/搜索