ReactDOM.render源碼解析-1

初步看了react-dom這個包的一些源碼,發現其比react包要複雜得多,react包中基本不存在跨包調用的狀況,他所作的也僅僅是定義了ReactElement對象,封裝了對ReactElement的基本操做,而react-dom包存在複雜的函數調用。本文將對ReactDOM.render源碼作一個初步解析。
文章中若有不當之處,歡迎交流指點。react版本 16.8.2。在源碼添加的註釋在github react-source-learn

前言

使用react時經常寫相似下面的代碼:html

import ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

代碼1react

這裏導入的ReactDOM就是packages/react-dom/client/ReactDOM.js中所導出的對象。從文檔可見ReactDOM對象有以下幾個方法:(ps:從源碼看其實還有不少其餘方法)git

  • render()
  • hydrate()
  • unmountComponentAtNode()
  • findDOMNode()
  • createPortal()

本文只介紹render()方法github

代碼分析

render方法定義以下:dom

render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    invariant(
      // 1
      isValidContainer(container),
      'Target container is not a DOM element.',
    );
    if (__DEV__) {
      warningWithoutStack(
        !container._reactHasBeenPassedToCreateRootDEV,
        'You are calling ReactDOM.render() on a container that was previously ' +
          'passed to ReactDOM.%s(). This is not supported. ' +
          'Did you mean to call root.render(element)?',
        enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
      );
    }
    // 2
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },

代碼2async

render方法接收兩個必選參數可一個可選參數,結合代碼1的調用可知,element是一個ReactElement對象, container是一個dom節點,callback在上面的代碼1並無指定,他是一個可選函數。函數

這個render方法作的事情比較簡單,一是校驗container參數,二是調用legacyRenderSubtreeIntoContainer方法並返回。spa

接下來是legacyRenderSubtreeIntoContainercode

// 刪除了第一次調ReactDOM.render不會走的分支
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>, // ReactDOM.render 是null
  children: ReactNodeList, // 是一個ReactElement , ReactDOM.render是第一個參數
  container: DOMContainer, // 是一個dom節點, ReactDOM.render是第二個參數
  forceHydrate: boolean, // ReactDOM.render 是false
  callback: ?Function, // ReactDOM.render 是 第三個參數
) {
  if (__DEV__) {
    topLevelUpdateWarnings(container);
  }

  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.
  // 根據type知道, Root type是個對象,包含
  // render方法
  // unmount方法
  // legacy_renderSubtreeIntoContainer 方法
  // createBatch 方法
  // _internalRoot屬性
  let root: Root = (container._reactRootContainer: any);
  if (!root) { // ReactDOM.render調用時走這裏
      // Initial mount
      // 調用 legacyCreateRootFromDOMContainer 拿 Root
      root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate, // ReactDOM.render是false
    );

    // 在callback加參數
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // 這個是packages/react-reconciler/ReactFiberScheduler.js中的方法
    // TOLEARN: 這個裏邊應該是一些調度過程, 後續再看
    unbatchedUpdates(() => {
      if (parentComponent != null) {
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else { // ReactDOM.render方法走這裏
        // 這裏的root.render 返回的是一個叫Work的東西, TOLEARN,這個Work後面再作了解
        root.render(children, callback);
      }
    });
  }
  // getPublicRootInstance是/packages/react-reconciler/ReactFiberReconciler.js中的方法
  // 關於他是返回的一個什麼東西, 後面再看, 總之他是我ReactDOM.render方法回調函數的一個參數,
  // 也是返回值
  return getPublicRootInstance(root._internalRoot);
}

legacyRenderSubtreeIntoContainer在第一次render時作了以下事情:htm

  1. 調用legacyCreateRootFromDOMContainer拿到一個ReactRoot的實例
  2. 在callback中注入一個參數instance
  3. 調用unbatchedUpdates開始一輪調度過程,這個是猜的
  4. 返回instance

關於instance的獲取是調用的getPublicRootInstance,getPublicRootInstance是/packages/react-reconciler/ReactFiberReconciler.js中的方法,後面再研究

關於unbatchedUpdates, 這個東西是這個是packages/react-reconciler/ReactFiberScheduler.js中的方法,簡單看了看還沒太明白

接下來看一下ReactRoot實例的獲取

// 刪除了__DEV__分支的代碼
// 若須要清理container的子節點,清理, 而後new ReactRoot並返回
function legacyCreateRootFromDOMContainer(
  container: DOMContainer,  // dom節點
  forceHydrate: boolean, // false render
): Root {
  // 是否不須要清理container的子元素, 第一次render是false, 即須要清理
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // 第一次調用時爲false
  // First clear any existing content.
  if (!shouldHydrate) { // 第一次render走這裏
    let warned = false;
    let rootSibling;
    // 這裏將container的子元素都清理掉了
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

這個方法比較簡單,在第一次調用ReactDOM.render時,shouldHydrate會是false,因此會走到if (!shouldHydrate) 分支裏,將container節點的全部子節點都清理掉,最後是new 了一個ReactRoot做爲返回值,關於ReactRoot等內容將放到後面的文章分析。

小結

函數調用過程

以上是ReactDOM.render的函數調用示意圖。

  • 首先在render方法中校驗container參數是否合法,而後調用legacyRenderSubtreeIntoContainer
  • 在legacyRenderSubtreeIntoContainer中, 調用legacyCreateRootFromDOMContainer拿到了ReactRoot的一個實例,調用getPublicRootInstance拿到了instance,用於注入到callback,和做爲返回值,調用unbatchedUpdates開始調度過程
  • 在legacyCreateRootFromDOMContainer中,首先清理了container中的全部子節點,而後new了一個ReactRoot並返回

TODO

  • unbatchedUpdates是如何調度的
  • ReactRoot是一個什麼樣的類
  • dom的操做是在哪裏