react源碼閱讀-react-domRender

  react-dom入口函數以及基本數據類型。閱讀React包的源碼版本爲16.8.6

  在第一章節我們瞭解到,react包本質上是一個數據結構建立的抽象屏障,提供起來供react的其它包,諸如react-domreact-native調用。在這一章中,進入react-dom的源碼閱讀。

  根據package.jsonmain字段入口,我們可以找到react-dom的入口文件爲src/client/ReactDOM.js。我們發現該文件最後的代碼export default ReactDOM僅僅對外暴露了一個對象模塊。我們簡單看一下這個對象模塊。

// 函數內部代碼均先省略
const ReactDOM: Object = {
  createPortal,
  findDOMNode() {  },
  hydrate() {},
  render() {},
  unstable_renderSubtreeIntoContainer() {},
  unmountComponentAtNode() {},
  unstable_createPortal() {},
  unstable_interactiveUpdates() {},
  unstable_discreteUpdates,
  unstable_flushDiscreteUpdates,
  flushSync,
  unstable_createRoot,
  unstable_createSyncRoot,
  unstable_flushControlled,
}

  其實這裏的對象模塊就是對面暴露的react-dom提供的Api部分。我們可以看到包括最熟悉的render方法,用於服務端渲染的hydrate,還有findDOMNodecreatePortal等。

  我們本章節就來查看下最常使用的render函數的源碼大體邏輯結構。

// 調用方式 ReactDOM.render(element, container[, callback])
render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    // 判斷dom節點是否正確
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  }

  react-dom源碼中使用了flow來定義數據類型,函數入參中如element: React$Element<any>這種寫法就是flow的語法。近似於typescript

  render函數在除去DEV調試部分邏輯後,剩餘的代碼非常簡單,判斷傳入的container節點是否爲Dom節點,是就進入legacyRenderSubtreeIntoContainer函數,我們來跟着代碼接着看。

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  // 是否複用dom節點,服務端渲染調用
  forceHydrate: boolean,
  callback: ?Function,
) {
  // 從 container 中獲得root節點
  let root: _ReactSyncRoot = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // 沒有root,創建root節點, 移除所有子節點
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    // 有無callback
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    // 有無callback 邏輯同上
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

  legacyRenderSubtreeIntoContainer首先取出container中的root節點,根據有無root節點來劃分不同的創建更新邏輯。首次使用render函數的時候是不存在root節點的,此時通過legacyCreateRootFromDOMContainer創建一個root節點給container._reactRootContainer。然後如果存在callback就進行調用,最後進行了一個unbatchedUpdates。存在root節點的時候,就省去了創建root節點部分的代碼,直接進行callback的判斷和updateContainer

  我們先來看創建root節點的legacyCreateRootFromDOMContainer部分的代碼。

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): _ReactSyncRoot {
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  // 不需要進行 shouldHydrate 過程,即我們正常的render過程
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    // 當有子節點的時候,一直循環,刪除完子節點
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not batched.
  /**
   * LegacyRoot 爲一個常量標識符,具體細節如下
   * export type RootTag = 0 | 1 | 2;
   * export const LegacyRoot = 0;
   * export const BatchedRoot = 1;
   * export const ConcurrentRoot = 2;
   */
  return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}

  前面提到過,forceHydrate這個布爾值是用於標識是否是服務端渲染的,在瀏覽器環境下是不觸碰這部分的邏輯的,這個相關部分就先跳過。那麼legacyCreateRootFromDOMContainer就做了兩件事情:

  1. 刪除container容器部分的所有子節點。這也就是爲什麼我們使用ReactDom.render渲染在目標節點之後,節點的子元素全部消失的原因。
  2. 返回了ReactSyncRoot類,實例化了一個root根節點的實例。

  接下來的ReactSyncRoot代碼更簡單:

function ReactSyncRoot(
  container: DOMContainer,
  tag: RootTag,
  hydrate: boolean,
) {
  // Tag is either LegacyRoot or Concurrent Root
  const root = createContainer(container, tag, hydrate);
  this._internalRoot = root;
}

  我們追尋createContainer函數,發現這個函數文件在react-reconciler/src/ReactFiberReconciler包中。我們跟着去查看一下:

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
): OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate);
}

// 在 `react-reconciler/src/ReactFiberRoot`文件中
export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
): FiberRoot {
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  const uninitializedFiber = createHostRootFiber(tag);
  // 相互指
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  return root;
}

// fiber root 結構的真身
function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  // root 節點對應的Fiber對象
  this.current = null;
  // dom 節點
  this.containerInfo = containerInfo;
  // 持久化更新會用到
  this.pendingChildren = null;
  this.pingCache = null;
  this.finishedExpirationTime = NoWork;
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.hydrate = hydrate;
  this.firstBatch = null;
  this.callbackNode = null;
  this.callbackExpirationTime = NoWork;
  this.firstPendingTime = NoWork;
  this.lastPendingTime = NoWork;
  this.pingTime = NoWork;

  if (enableSchedulerTracing) {
    this.interactionThreadID = unstable_getThreadID();
    this.memoizedInteractions = new Set();
    this.pendingInteractionMap = new Map();
  }
}

  終於在FiberRootNode中發現了rootRoot的真身,就是一個帶標識的對象。其中比較重要的一個爲containerInfo,就是reactElement將要渲染上的容器節點信息。我們還能發現,很多標識賦值了NoWorkNoWork設計到後續我們更新會提及的ExpirationTime的概念,是React更新算法的基礎。目前你可以就把NoWork理解爲一個標識0的常量(源碼export const NoWork = 0;)。

  我們最後來看current,在createFiberRoot中將其指向了createHostRootFiber創建的uninitializedFiber。這個uninitializedFiber就是reactElement對應的fiber節點,我們一起來看一下這部分邏輯。

// 位於react-reconciler/src/ReactFiber.js
function createHostRootFiber(tag: RootTag): Fiber {
  let mode;
  // 根據 tag 的不同,獲得不同的mode模式
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode | BatchedMode | StrictMode;
  } else if (tag === BatchedRoot) {
    mode = BatchedMode | StrictMode;
  } else {
    mode = NoMode;
  }

  if (enableProfilerTimer && isDevToolsPresent) {
    // Always collect profile timings when DevTools are present.
    // This enables DevTools to start capturing timing at any point–
    // Without some nodes in the tree having empty base times.
    mode |= ProfileMode;
  }

  return createFiber(HostRoot, null, null, mode);
}

const createFiber = function(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  return new FiberNode(tag, pendingProps, key, mode);
};

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;
  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  // pendingProps 將要更新
  this.pendingProps = pendingProps;
  // 之前的props
  this.memoizedProps = null;
  // update對象
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode;
  // Effects,標記組件生命週期,以及組件是否需要更新
  this.effectTag = NoEffect;
  this.nextEffect = null;
  this.firstEffect = null;
  this.lastEffect = null;
  this.expirationTime = NoWork;
  this.childExpirationTime = NoWork;
  this.alternate = null;

  if (enableProfilerTimer) {
    this.actualDuration = Number.NaN;
    this.actualStartTime = Number.NaN;
    this.selfBaseDuration = Number.NaN;
    this.treeBaseDuration = Number.NaN;
    // It's okay to replace the initial doubles with smis after initialization.
    // This won't trigger the performance cliff mentioned above,
    // and it simplifies other profiler code (including DevTools).
    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }
}

  這部分邏輯比較長,我們來拆成兩部來看。createHostRootFiber總共做了兩件事情,根據tag存在的標識,調整了mode字段。然後使用mode字段創建了FiberNode對象。

  這裏我們稍微提一下使用|&來進行一個打標的設計模式。比如我現在有三個屬性的標識符a/b/c,我們用二進制來定義它們,保證每個模式1所在的位置不同。

var a = 0b001;
var b = 0b010;
var c = 0b100;

  我們現在對一個demo變量進行屬性的賦值,比如我想要這個demo變量擁有屬性a屬性c。那我只需要var demo = a | c。在後續我對demo進行一個擁有屬性判斷的時候,我只需要使用&,如果得到的結果大於0,即轉換爲true,就說明demo擁有該屬性。如我想要判斷demo是否含有a屬性,只需要if (demo | a) { /* ... */ }即可。如果我想要給demo添加一個屬性,比如添加屬性b,只需要將demo |= b即可,如果不是很瞭解一元操作符的同學,可以去mdn上面查一下相關的資料就能明白。

  我們前面在legacyCreateRootFromDOMContainer函數的註釋中提到過,rootTag是通過LegacyRoot | BatchedRoot | ConcurrentRoot取得的三個模式的綜合。所以createHostRootFiber這裏我們走的是最後一個else分支,mode=NoMode。然後創建Fiber節點。

  Fiber節點就是對應每一個ReactElement的節點了,它上面記載了很多我們熟悉的屬性,比如ref,比如props相關的pendingPropsmemoizedProps。然後還需要關注一下的概念就是expirationTimeexpirationTime前面root的時候也提到了,這是節點更新操作的依據,在後續的源碼部分也會單獨拆分一章節來闡述它。

  還需要提一下的是我註釋了Fiber相關的幾個屬性sibling,return,childreact中的Fiber節點對應的是一個單向列表結構。比如我有這樣的一個jsx結構:

function Demo() {
  return (
    <ul>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  );
}

  那麼這個結構在Fiber中會這樣存在ul.child -> li(1).sibling -> li(2).sibling -> li(3)。每個節點的return則對應總的父節點li(1).return -> ul

  這一章當中,我們簡單看了一下ReactDom.render的總體函數邏輯和創建數據結構部分的源碼。首次創建的時候,render會創建一個FiberRootNode對象,該對象作爲整個React應用的根節點,同時給RootNode創建對應的Fiber對象。每一個Fiber對象對應一個ReactElement元素,它帶有各種用於React調度的屬性元素,DOM以單向鏈表的數據結存在於React應用當中。

  下一章我們會接着render函數的邏輯進入unbatchedUpdates部分代碼,大體介紹一下React-dom在更新中的一些框架設計。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章