React源碼解析之ReactDOM.render()

一、React更新的方式有三種:
(1)ReactDOM.render() || hydrate(ReactDOMServer渲染)
(2)setState
(3)forceUpdate

接下來,我們就來看下ReactDOM.render()源碼

二、ReactDOM.render(element, container[, callback])

作用:
在提供的container裏渲染一個React元素,並返回對該組件的引用

常見的用法是這個:

ReactDOM.render(<App />, document.getElementById('root'));

官網網址:
https://zh-hans.reactjs.org/docs/react-dom.html#render

源碼:

const ReactDOM: Object = {
  //服務端使用hydrate方法渲染節點
  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );

    // TODO: throw or warn if we couldn't hydrate?
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      //true是讓服務端儘可能複用節點,提高性能
      true,
      callback,
    );
  },

  render(
    //元素
    element: React$Element<any>,
    //容器
    container: DOMContainer,
    //應用渲染結束後,調用的函數
    callback: ?Function,
  ) {
    //錯誤抓取
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );

    //render方法本質是返回了函數legacyRenderSubtreeIntoContainer
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      //render不會複用節點,因爲是前端渲染
      false,
      callback,
    );
  },

}

解析:
(1)render()方法本質是返回了函數legacyRenderSubtreeIntoContainer()

(2)hydrate()render()的唯一區別是傳入legacyRenderSubtreeIntoContainer()的第四個參數不一樣:
hydrate()true,表示在服務端儘可能複用節點,提高性能;
render()false,表示在瀏覽器端不會去複用節點,而是全部替換掉。

三、legacyRenderSubtreeIntoContainer()

作用:
初始化Container

源碼:

// null, element, container, false, callback,
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.

  //render中一般渲染的是DOM標籤,所以不會有_reactRootContainer存在,
  // 所以第一次渲染,root是不存在的
  let root: _ReactSyncRoot = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // Initial mount
    //創建一個ReactRooter
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;

    //判斷是否有callback
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        //根據fiberRoot獲取公共Root實例
        //就是fiberRoot.current.child.stateNode
        const instance = getPublicRootInstance(fiberRoot);
        //通過該實例instance 去調用originalCallback方法
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    //初始化安裝不應該批量更新
    unbatchedUpdates(() => {
      //element,fiberRoot,null,callback
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    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);
}

解析:
(1)由於是第一次渲染更新,所以rootnull,只需看!root的情況

(2)legacyCreateRootFromDOMContainer(container,false,)的作用是創建ReactRooter,稍後會講解

(3)unbatchedUpdates(fn)的簡化源碼如下:

unbatchedUpdates(fn){
  return fn()
}

(4)updateContainer()的作用是更新container,稍後講解

四、legacyCreateRootFromDOMContainer(container,forceHydrate,)

作用:
創建一個ReactRooter

源碼:

//創建ReactRooter
function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): _ReactSyncRoot {
  //是否是服務端渲染
  const shouldHydrate =
    //render的forceHydrate是false,所以會調用shouldHydrateDueToLegacyHeuristic方法來判斷是否是服務端渲染
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  //如果不是服務端渲染的話
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    //循環刪除container的子節點
    //爲什麼要刪除?因爲React認爲這些節點是不需要複用的
    while ((rootSibling = container.lastChild)) {
 
      container.removeChild(rootSibling);
    }
  }

  // Legacy roots are not batched.
  //container是空的container,0,false
  //ReactRoot是同步的
  //sync 同步
  //async 異步
  return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}

解析:
(1)render()forceHydratefalse,所以看shouldHydrateDueToLegacyHeuristic(container)是否返回false

shouldHydrateDueToLegacyHeuristic()

作用:
判斷是否是服務端渲染

源碼:

//判斷是否是服務端渲染
function shouldHydrateDueToLegacyHeuristic(container) {
  //獲取container的第一個節點(根節點)
  //也就是 id='root' 的節點
  const rootElement = getReactRootElementInContainer(container);
  return !!(
    rootElement &&
    rootElement.nodeType === ELEMENT_NODE &&
    //判斷是否是服務端渲染
    rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
  );
}

getReactRootElementInContainer()

作用:
獲取container中的第一個節點(或文檔節點)

源碼:

//獲取Container裏的RectRoot元素
//返回document節點或第一個子節點
function getReactRootElementInContainer(container: any) {
  if (!container) {
    return null;
  }
  //DOCUMENT_NODE 即 window.document
  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}

也就是說,判斷是否是服務端渲染的標誌是:
在獲取container中的第一個節點(或文檔節點)後,看該節點是否有屬性ROOT_ATTRIBUTE_NAME

ROOT_ATTRIBUTE_NAME是什麼呢?

//服務端渲染的話,會在React App的第一個元素上添加該屬性
//以標識是服務端渲染的
export const ROOT_ATTRIBUTE_NAME = 'data-reactroot';

data-reactroot

(2)由(1)可知,render()container的首節點是沒有data-reactroot屬性的,所以會進行while循環,依次刪除container的子節點,刪除完畢後,new 一個ReactSyncRoot()的實例

(3)ReactSyncRoot()

作用:
創建ReactRoot實例

源碼:

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

把創建的root作爲legacyCreateRootFromDOMContainer()__internalRoot屬性

createContainer

作用:
創建React容器

源碼:

//創建React容器
export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
): OpaqueRoot {
  //創建FiberRoot
  return createFiberRoot(containerInfo, tag, hydrate);
}

也就是說legacyCreateRootFromDOMContainer()的本質是創建了FilberRoot

五、updateContainer()

作用:
創建更新container

源碼:

//更新Container
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  //1073741823
  const currentTime = requestCurrentTime();

  const suspenseConfig = requestCurrentSuspenseConfig();
  //計算過期時間,這是React優先級更新非常重要的點
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    suspenseConfig,
    callback,
  );
}

解析:

(1)requestCurrentTime()

作用:
計算新開始的時間

源碼不用看,只需要知道該時間,是以V8引擎上最大31位整數1073741823爲根據的:

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
//整型最大數值,是V8中針對32位系統所設置的最大值
export default 1073741823;

(2)requestCurrentSuspenseConfig()computeExpirationForFiber()以後會講解

(3)updateContainerAtExpirationTime()

作用:
每到過期時間,就更新container,過期時間單位爲 10ms

源碼:

//在過期時間內,更新container
export function updateContainerAtExpirationTime(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
  callback: ?Function,
) {
  // TODO: If this is a nested container, this won't be the root.
  const current = container.current;

  //由於parentComponent爲null,所以返回空對象{}
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }
  //計劃更新Root
  return scheduleRootUpdate(
    current,
    element,
    expirationTime,
    suspenseConfig,
    callback,
  );
}

解析:

scheduleRootUpdate()

作用:
計劃更新Root

源碼:

//計劃更新Root
function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
  callback: ?Function,
) {

  //創建更新的時間節點
  const update = createUpdate(expirationTime, suspenseConfig);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    warningWithoutStack(
      typeof callback === 'function',
      'render(...): Expected the last optional `callback` argument to be a ' +
        'function. Instead received: %s.',
      callback,
    );
    update.callback = callback;
  }

  if (revertPassiveEffectsChange) {
    flushPassiveEffects();
  }
  //一整個React應用中,會有多次更新,而這多次更新均在更新隊列中
  enqueueUpdate(current, update);
  //進行任務調度
  //當React進行Update後,就要進行調度
  //即 根據任務的優先級去調度任務
  //先執行優先級高的任務,
  scheduleWork(current, expirationTime);

  return expirationTime;
}

解析:
任務調度是React中最重要、複雜的內容,之後會慢慢來解析。
這裏可以看到,React將初始化的Update放入了更新隊列中,並進行任務調度,最終返回了一個expirationTime

也就是說,updateContainer()本質是返回了expirationTime

六、getPublicRootInstance()

作用:
獲取root實例

源碼:

//獲取root實例
export function getPublicRootInstance(
  container: OpaqueRoot,
): React$Component<any, any> | PublicInstance | null {
  //看到container.current,我就想到了ref(xxx.current)
  //獲取當前節點
  const containerFiber = container.current;
  if (!containerFiber.child) {
    return null;
  }
  switch (containerFiber.child.tag) {
    case HostComponent:
      return getPublicInstance(containerFiber.child.stateNode);
    default:
      return containerFiber.child.stateNode;
  }
}

解析:
由於是 React 初始化,所以container.current是沒有子節點的,所以該方法返回 null

七、ReactDOM.render()流程圖

總結:
ReactDOM.render() 的更新步驟
(1)創建 ReactRoot,ReactRoot 是創建整個React應用的根對象

(2)創建 FiberRoot 和 RootFiber

(3)創建更新 (創建更新後,就會進入調度階段,調度階段由調度器進行管理)

GitHub:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/ReactDOM.js


(完)

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