React源碼解析之ReactDOM.render()

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

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

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

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

常見的用法是這個:java

ReactDOM.render(<App />, document.getElementById('root'));
複製代碼

官網網址:
zh-hans.reactjs.org/docs/react-…node

源碼:react

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()git

(2)hydrate()render()的惟一區別是傳入legacyRenderSubtreeIntoContainer()的第四個參數不同:
hydrate()true,表示在服務端儘量複用節點,提升性能;
render()false,表示在瀏覽器端不會去複用節點,而是所有替換掉。github

3、legacyRenderSubtreeIntoContainer()web

做用:
初始化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,稍後講解

4、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

5、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

6、getPublicRootInstance()

做用:
獲取root實例

源碼:

//獲取root實例
export function getPublicRootInstance(
  container: OpaqueRoot,
): React$Component<anyany> | 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

7、ReactDOM.render()流程圖

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

(2)建立 FiberRoot 和 RootFiber

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

GitHub:
github.com/AttackXiaoJ…


(完)

相關文章
相關標籤/搜索