1. 程式人生 > >React16原始碼解讀:揭祕ReactDOM.render

React16原始碼解讀:揭祕ReactDOM.render

引言

在上一篇文章中我們通過create-react-app腳手架快速搭建了一個簡單的示例,並基於該示例講解了在類元件中React.ComponentReact.PureComponent背後的實現原理。同時我們也瞭解到,通過使用Babel預置工具包@babel/preset-react可以將類元件中render方法的返回值和函式定義元件中的返回值轉換成使用React.createElement方法包裝而成的多層巢狀結構,並基於原始碼逐行分析了React.createElement方法背後的實現過程和ReactElement建構函式的成員結構,最後根據分析結果總結出了幾道面試中可能會碰到或者自己以前遇到過的面試考點。上篇文章中的內容相對而言還是比較簡單基礎,主要是為本文以及後續的任務排程相關內容打下基礎,幫助我們更好地理解原始碼的用意。本文就結合上篇文章的基礎內容,從元件渲染的入口點ReactDOM.render

方法開始,一步一步深入原始碼,揭祕ReactDOM.render方法背後的實現原理,如有錯誤,還請指出。

原始碼中有很多判斷類似__DEV__變數的控制語句,用於區分開發環境和生產環境,筆者在閱讀原始碼的過程中不太關心這些內容,就直接略過了,有興趣的小夥伴兒可以自己研究研究。

render VS hydrate

本系列的原始碼分析是基於Reactv16.10.2版本的,為了保證原始碼一致還是建議你選擇相同的版本,下載該版本的地址和筆者選擇該版本的具體原因可以在上篇文章的準備階段小節中檢視,這裡就不做過多講解了。專案示例本身也比較簡單,可以按照準備階段的步驟自行使用create-react-app

快速將一個簡單的示例搭建起來,然後我們定位到src/index.js檔案下,可以看到如下程式碼:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
...
ReactDOM.render(<App />, document.getElementById('root'));
...

該檔案即為專案的主入口檔案,App元件即為根元件,ReactDOM.render就是我們要開始分析原始碼的入口點。我們通過以下路徑可以找到ReactDOM

物件的完整程式碼:

packages -> react-dom -> src -> client -> ReactDOM.js

然後我們將程式碼定位到第632行,可以看到ReactDOM物件包含了很多我們可能使用過的方法,例如rendercreatePortalfindDOMNodehydrateunmountComponentAtNode等。本文中我們暫且只關心render方法,但為了方便對比,也可以簡單看下hydrate方法:

const ReactDOM: Object = {
  ...
  /**
   * 服務端渲染
   * @param element 表示一個ReactNode,可以是一個ReactElement物件
   * @param container 需要將元件掛載到頁面中的DOM容器
   * @param callback 渲染完成後需要執行的回撥函式
   */
  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?
    // 注意第一個引數為null,第四個引數為true
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      true,
      callback,
    );
  },

  /**
   * 客戶端渲染
   * @param element 表示一個ReactElement物件
   * @param container 需要將元件掛載到頁面中的DOM容器
   * @param callback 渲染完成後需要執行的回撥函式
   */
  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );
    ...
    // 注意第一個引數為null,第四個引數為false
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },
  ...
};

發現沒,render方法的第一個引數就是我們在上篇文章中講過的ReactElement物件,所以說上篇文章的內容就是為了在這裡打下基礎的,便於我們對引數的理解。事實上,在原始碼中幾乎所有方法引數中的element欄位均可以傳入一個ReactElement例項,這個例項就是通過Babel編譯器在編譯過程中使用React.createElement方法得到的。接下來在render方法中呼叫legacyRenderSubtreeIntoContainer來正式進入渲染流程,不過這裡需要留意一下的是,render方法和hydrate方法在執行legacyRenderSubtreeIntoContainer時,第一個引數的值均為null,第四個引數的值恰好相反。

然後將程式碼定位到第570行,進入legacyRenderSubtreeIntoContainer方法的具體實現:

/**
 * 開始構建FiberRoot和RootFiber,之後開始執行更新任務
 * @param parentComponent 父元件,可以把它當成null值來處理
 * @param children ReactDOM.render()或者ReactDOM.hydrate()中的第一個引數,可以理解為根元件
 * @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二個引數,元件需要掛載的DOM容器
 * @param forceHydrate 表示是否融合,用於區分客戶端渲染和服務端渲染,render方法傳false,hydrate方法傳true
 * @param callback ReactDOM.render()或者ReactDOM.hydrate()中的第三個引數,元件渲染完成後需要執行的回撥函式
 * @returns {*}
 */
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.
  // 在第一次執行的時候,container上是肯定沒有_reactRootContainer屬性的
  // 所以第一次執行時,root肯定為undefined
  let root: _ReactSyncRoot = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // Initial mount
    // 首次掛載,進入當前流程控制中,container._reactRootContainer指向一個ReactSyncRoot例項
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    // root表示一個ReactSyncRoot例項,例項中有一個_internalRoot方法指向一個fiberRoot例項
    fiberRoot = root._internalRoot;
    // callback表示ReactDOM.render()或者ReactDOM.hydrate()中的第三個引數
    // 重寫callback,通過fiberRoot去找到其對應的rootFiber,然後將rootFiber的第一個child的stateNode作為callback中的this指向
    // 一般情況下我們很少去寫第三個引數,所以可以不必關心這裡的內容
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // 對於首次掛載來說,更新操作不應該是批量的,所以會先執行unbatchedUpdates方法
    // 該方法中會將executionContext(執行上下文)切換成LegacyUnbatchedContext(非批量上下文)
    // 切換上下文之後再呼叫updateContainer執行更新操作
    // 執行完updateContainer之後再將executionContext恢復到之前的狀態
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    // 不是首次掛載,即container._reactRootContainer上已經存在一個ReactSyncRoot例項
    fiberRoot = root._internalRoot;
    // 下面的控制語句和上面的邏輯保持一致
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    // 對於非首次掛載來說,是不需要再呼叫unbatchedUpdates方法的
    // 即不再需要將executionContext(執行上下文)切換成LegacyUnbatchedContext(非批量上下文)
    // 而是直接呼叫updateContainer執行更新操作
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

上面程式碼的內容稍微有些多,咋一看可能不太好理解,我們暫且可以不用著急看完整個函式內容。試想當我們第一次啟動執行專案的時候,也就是第一次執行ReactDOM.render方法的時候,這時去獲取container._reactRootContainer肯定是沒有值的,所以我們先關心第一個if語句中的內容:

if (!root) {
    // Initial mount
    // 首次掛載,進入當前流程控制中,container._reactRootContainer指向一個ReactSyncRoot例項
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    ...
}

這裡通過呼叫legacyCreateRootFromDOMContainer方法將其返回值賦值給container._reactRootContainer,我們將程式碼定位到同文件下的第517行,去看看legacyCreateRootFromDOMContainer的具體實現:

/**
 * 建立並返回一個ReactSyncRoot例項
 * @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二個引數,元件需要掛載的DOM容器
 * @param forceHydrate 是否需要強制融合,render方法傳false,hydrate方法傳true
 * @returns {ReactSyncRoot}
 */
function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): _ReactSyncRoot {
  // 判斷是否需要融合
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  // 針對客戶端渲染的情況,需要將container容器中的所有元素移除
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    // 迴圈遍歷每個子節點進行刪除
    while ((rootSibling = container.lastChild)) {
      ...
      container.removeChild(rootSibling);
    }
  }
  ...
  // Legacy roots are not batched.
  // 返回一個ReactSyncRoot例項
  // 該例項具有一個_internalRoot屬性指向fiberRoot
  return new ReactSyncRoot(
    container,
    LegacyRoot,
    shouldHydrate
      ? {
          hydrate: true,
        }
      : undefined,
  );
}

/**
 * 根據nodeType和attribute判斷是否需要融合
 * @param container DOM容器
 * @returns {boolean}
 */
function shouldHydrateDueToLegacyHeuristic(container) {
  const rootElement = getReactRootElementInContainer(container);
  return !!(
    rootElement &&
    rootElement.nodeType === ELEMENT_NODE &&
    rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
  );
}

/**
 * 根據container來獲取DOM容器中的第一個子節點
 * @param container DOM容器
 * @returns {*}
 */
function getReactRootElementInContainer(container: any) {
  if (!container) {
    return null;
  }

  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}

其中在shouldHydrateDueToLegacyHeuristic方法中,首先根據container來獲取DOM容器中的第一個子節點,獲取該子節點的目的在於通過節點的nodeType和是否具有ROOT_ATTRIBUTE_NAME屬性來區分是客戶端渲染還是服務端渲染,ROOT_ATTRIBUTE_NAME位於packages/react-dom/src/shared/DOMProperty.js檔案中,表示data-reactroot屬性。我們知道,在服務端渲染中有別於客戶端渲染的是,node服務會在後臺先根據匹配到的路由生成完整的HTML字串,然後再將HTML字串傳送到瀏覽器端,最終生成的HTML結構簡化後如下:

<body>
    <div id="root">
        <div data-reactroot=""></div>
    </div>
</body>

在客戶端渲染中是沒有data-reactroot屬性的,因此就可以區分出客戶端渲染和服務端渲染。在React中的nodeType主要包含了五種,其對應的值和W3C中的nodeType標準是保持一致的,位於與DOMProperty.js同級的HTMLNodeType.js檔案中:

// 代表元素節點
export const ELEMENT_NODE = 1;
// 代表文字節點
export const TEXT_NODE = 3;
// 代表註釋節點
export const COMMENT_NODE = 8;
// 代表整個文件,即document
export const DOCUMENT_NODE = 9;
// 代表文件片段節點
export const DOCUMENT_FRAGMENT_NODE = 11;

經過以上分析,現在我們就可以很容易地區分出客戶端渲染和服務端渲染,並且在面試中如果被問到兩種渲染模式的區別,我們就可以很輕鬆地在原始碼級別上說出兩者的實現差異,讓面試官眼前一亮。怎麼樣,到目前為止,其實還是覺得挺簡單的吧?

FiberRoot VS RootFiber

在這一小節中,我們將嘗試去理解兩個比較容易混淆的概念:FiberRootRootFiber。這兩個概念在React的整個任務排程過程中起著關鍵性的作用,如果不理解這兩個概念,後續的任務排程過程就是空談,所以這裡也是我們必須要去理解的部分。接下來接著上一小節的內容,繼續分析legacyCreateRootFromDOMContainer方法中的剩餘內容,在函式體的結尾返回了一個ReactSyncRoot例項,我們重新回到ReactDOM.js檔案可以很容易找到ReactSyncRoot建構函式的具體內容:

/**
 * ReactSyncRoot建構函式
 * @param container DOM容器
 * @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param options 配置資訊,只有在hydrate時才有值,否則為undefined
 * @constructor
 */
function ReactSyncRoot(
  container: DOMContainer,
  tag: RootTag,
  options: void | RootOptions,
) {
  this._internalRoot = createRootImpl(container, tag, options);
}

/**
 * 建立並返回一個fiberRoot
 * @param container DOM容器
 * @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param options 配置資訊,只有在hydrate時才有值,否則為undefined
 * @returns {*}
 */
function createRootImpl(
  container: DOMContainer,
  tag: RootTag,
  options: void | RootOptions,
) {
  // Tag is either LegacyRoot or Concurrent Root
  // 判斷是否是hydrate模式
  const hydrate = options != null && options.hydrate === true;
  const hydrationCallbacks =
    (options != null && options.hydrationOptions) || null;
  
  // 建立一個fiberRoot
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // 給container附加一個內部屬性用於指向fiberRoot的current屬性對應的rootFiber節點
  markContainerAsRoot(root.current, container);
  if (hydrate && tag !== LegacyRoot) {
    const doc =
      container.nodeType === DOCUMENT_NODE
        ? container
        : container.ownerDocument;
    eagerlyTrapReplayableEvents(doc);
  }
  return root;
}

從上述原始碼中,我們可以看到createRootImpl方法通過呼叫createContainer方法來建立一個fiberRoot例項,並將該例項返回並賦值到ReactSyncRoot建構函式的內部成員_internalRoot屬性上。我們繼續深入createContainer方法去探究一下fiberRoot完整的建立過程,該方法被抽取到與react-dom包同級的另一個相關的依賴包react-reconciler包中,然後定位到react-reconciler/src/ReactFiberReconciler.js的第299行:

/**
 * 內部呼叫createFiberRoot方法返回一個fiberRoot例項
 * @param containerInfo DOM容器
 * @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param hydrate 判斷是否是hydrate模式
 * @param hydrationCallbacks 只有在hydrate模式時才可能有值,該物件包含兩個可選的方法:onHydrated和onDeleted
 * @returns {FiberRoot}
 */
export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

/**
 * 建立fiberRoot和rootFiber並相互引用
 * @param containerInfo DOM容器
 * @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param hydrate 判斷是否是hydrate模式
 * @param hydrationCallbacks 只有在hydrate模式時才可能有值,該物件包含兩個可選的方法:onHydrated和onDeleted
 * @returns {FiberRoot}
 */
export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
  // 通過FiberRootNode建構函式建立一個fiberRoot例項
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  // 通過createHostRootFiber方法建立fiber tree的根節點,即rootFiber
  // 需要留意的是,fiber節點也會像DOM樹結構一樣形成一個fiber tree單鏈表樹結構
  // 每個DOM節點或者元件都會生成一個與之對應的fiber節點(生成的過程會在後續的文章中進行解讀)
  // 在後續的調和(reconciliation)階段起著至關重要的作用
  const uninitializedFiber = createHostRootFiber(tag);
  // 建立完rootFiber之後,會將fiberRoot例項的current屬性指向剛建立的rootFiber
  root.current = uninitializedFiber;
  // 同時rootFiber的stateNode屬性會指向fiberRoot例項,形成相互引用
  uninitializedFiber.stateNode = root;
  // 最後將建立的fiberRoot例項返回
  return root;
}

一個完整的FiberRootNode例項包含了很多有用的屬性,這些屬性在任務排程階段都發揮著各自的作用,可以在ReactFiberRoot.js檔案中看到完整的FiberRootNode建構函式的實現(這裡只列舉部分屬性):

/**
 *  FiberRootNode建構函式
 * @param containerInfo DOM容器
 * @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param hydrate 判斷是否是hydrate模式
 * @constructor
 */
function FiberRootNode(containerInfo, tag, hydrate) {
  // 用於標記fiberRoot的型別
  this.tag = tag;
  // 指向當前啟用的與之對應的rootFiber節點
  this.current = null;
  // 和fiberRoot關聯的DOM容器的相關資訊
  this.containerInfo = containerInfo;
  ...
  // 當前的fiberRoot是否處於hydrate模式
  this.hydrate = hydrate;
  ...
  // 每個fiberRoot例項上都只會維護一個任務,該任務儲存在callbackNode屬性中
  this.callbackNode = null;
  // 當前任務的優先順序
  this.callbackPriority = NoPriority;
  ...
}

部分屬性資訊如上所示,由於屬性過多並且在本文中暫時還用不到,這裡就先不一一列舉出來了,剩餘的屬性及其註釋資訊已經上傳至Github,感興趣的朋友可以自行檢視。在瞭解完了fiberRoot的屬性結構之後,接下來繼續探究createFiberRoot方法的後半部分內容:

// 以下程式碼來自上文中的createFiberRoot方法
// 通過createHostRootFiber方法建立fiber tree的根節點,即rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 建立完rootFiber之後,會將fiberRoot例項的current屬性指向剛建立的rootFiber
root.current = uninitializedFiber;
// 同時rootFiber的stateNode屬性會指向fiberRoot例項,形成相互引用
uninitializedFiber.stateNode = root;

// 以下程式碼來自ReactFiber.js檔案
/**
 * 內部呼叫createFiber方法建立一個FiberNode例項
 * @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @returns {Fiber}
 */
export function createHostRootFiber(tag: RootTag): Fiber {
  let mode;
  // 以下程式碼根據fiberRoot的標記型別來動態設定rootFiber的mode屬性
  // export const NoMode = 0b0000;          => 0
  // export const StrictMode = 0b0001;      => 1
  // export const BatchedMode = 0b0010;     => 2
  // export const ConcurrentMode = 0b0100;  => 4
  // export const ProfileMode = 0b1000;     => 8
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode | BatchedMode | StrictMode;
  } else if (tag === BatchedRoot) {
    mode = BatchedMode | StrictMode;
  } else {
    mode = NoMode;
  }
  ...

  // 呼叫createFiber方法建立並返回一個FiberNode例項
  // HostRoot表示fiber tree的根節點
  // 其他標記型別可以在shared/ReactWorkTags.js檔案中找到
  return createFiber(HostRoot, null, null, mode);
}

/**
 * 建立並返回一個FiberNode例項
 * @param tag 用於標記fiber節點的型別(所有的型別存放在shared/ReactWorkTags.js檔案中)
 * @param pendingProps 表示待處理的props資料
 * @param key 用於唯一標識一個fiber節點(特別在一些列表資料結構中,一般會要求為每個DOM節點或元件加上額外的key屬性,在後續的調和階段會派上用場)
 * @param mode 表示fiber節點的模式
 * @returns {FiberNode}
 */
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
  // FiberNode建構函式用於建立一個FiberNode例項,即一個fiber節點
  return new FiberNode(tag, pendingProps, key, mode);
};

至此我們就成功地建立了一個fiber節點,上文中我們提到過,和DOM樹結構類似,fiber節點也會形成一個與DOM樹結構對應的fiber tree,並且是基於單鏈表的樹結構,我們在上面剛建立的fiber節點可作為整個fiber tree的根節點,即RootFiber節點。在目前階段,我們暫時不用關心一個fiber節點所包含的所有屬性,但可以稍微留意一下以下相關屬性:

/**
 * FiberNode建構函式
 * @param tag 用於標記fiber節點的型別
 * @param pendingProps 表示待處理的props資料
 * @param key 用於唯一標識一個fiber節點(特別在一些列表資料結構中,一般會要求為每個DOM節點或元件加上額外的key屬性,在後續的調和階段會派上用場)
 * @param mode 表示fiber節點的模式
 * @constructor
 */
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  // 用於標記fiber節點的型別
  this.tag = tag;
  // 用於唯一標識一個fiber節點
  this.key = key;
  ...
  // 對於rootFiber節點而言,stateNode屬性指向對應的fiberRoot節點
  // 對於child fiber節點而言,stateNode屬性指向對應的元件例項
  this.stateNode = null;

  // Fiber
  // 以下屬性建立單鏈表樹結構
  // return屬性始終指向父節點
  // child屬性始終指向第一個子節點
  // sibling屬性始終指向第一個兄弟節點
  this.return = null;
  this.child = null;
  this.sibling = null;
  // index屬性表示當前fiber節點的索引
  this.index = 0;
  ...

  // 表示待處理的props資料
  this.pendingProps = pendingProps;
  // 表示之前已經儲存的props資料
  this.memoizedProps = null;
  // 表示更新佇列
  // 例如在常見的setState操作中
  // 其實會先將需要更新的資料存放到這裡的updateQueue佇列中用於後續排程
  this.updateQueue = null;
  // 表示之前已經儲存的state資料
  this.memoizedState = null;
  ...

  // 表示fiber節點的模式
  this.mode = mode;

  // 表示當前更新任務的過期時間,即在該時間之後更新任務將會被完成
  this.expirationTime = NoWork;
  // 表示當前fiber節點的子fiber節點中具有最高優先順序的任務的過期時間
  // 該屬性的值會根據子fiber節點中的任務優先順序進行動態調整
  this.childExpirationTime = NoWork;

  // 用於指向另一個fiber節點
  // 這兩個fiber節點使用alternate屬性相互引用,形成雙緩衝
  // alternate屬性指向的fiber節點在任務排程中又稱為workInProgress節點
  this.alternate = null;
  ...
}

其他有用的屬性筆者已經在原始碼中寫好相關注釋,感興趣的朋友可以在Github上檢視完整的註釋資訊幫助理解。當然在現階段,其中的一些屬性還暫時難以理解,不過沒有關係,在後續的內容和系列文章中將會逐個擊破。在本小節中我們主要是為了理解FiberRootRootFiber這兩個容易混淆的概念以及兩者之間的聯絡。同時在這裡我們需要特別注意的是,多個fiber節點可形成基於單鏈表的樹形結構,通過自身的returnchildsibling屬性可以在多個fiber節點之間建立聯絡。為了更加容易理解多個fiber節點及其屬性之間的關係,這裡先回顧一下在上一篇文章中的簡單示例,我們在src/App.js檔案中將create-react-app腳手架生成的預設根元件App修改為如下形式:

import React, {Component} from 'react';

function List({data}) {
    return (
        <ul className="data-list">
            {
                data.map(item => {
                    return <li className="data-item" key={item}>{item}</li>
                })
            }
        </ul>
    );
}

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            data: [1, 2, 3]
        };
    }

    render() {
        return (
            <div className="container">
                <h1 className="title">React learning</h1>
                <List data={this.state.data} />
            </div>
        );
    }
}

最終生成的DOM結構如下所示:

<div class="container">
    <h1 class="title">React learning</h1>
    <ul class="data-list">
        <li class="data-item">1</li>
        <li class="data-item">2</li>
        <li class="data-item">3</li>
    </ul>
</div>

基於該DOM結構再結合上文中對原始碼的分析過程,最後我們可以嘗試得出一張關係圖來加深印象:

總結

本文主要是在上一篇文章內容的基礎之上從零開始逐行分析ReactDOM.render方法的實現原理,其背後的實現過程和呼叫棧還是非常複雜的,自己也是處於不斷的摸索過程中。在本文中主要是介紹兩個核心概念:FiberRootRootFiber,只有理解並區分這兩個概念之後才能更好地理解React的Fiber架構和任務排程階段中任務的執行過程。閱讀原始碼的過程是痛苦的,但與此同時自己所獲得的收益也是巨大的,為了避免文章過於枯燥,還是打算將原始碼內容劃分到系列文章中來單獨解讀,期間的間隔時間可用於對之前內容進行回顧,避免一口吃個胖子反而效果不好。

感謝閱讀

如果你覺得這篇文章的內容對你有幫助,能否幫個忙關注一下筆者的公眾號[前端之境],每週都會努力原創一些前端技術乾貨,關注公眾號後可以邀你加入前端技術交流群,我們可以一起互相交流,共同進步。

文章已同步更新至Github部落格,若覺文章尚可,歡迎前往star!

你的一個點贊,值得讓我付出更多的努力!

逆境中成長,只有不斷地學習,才能成為更好的自己,與君共勉!

相關推薦

React16原始碼解讀揭祕ReactDOM.render

引言 在上一篇文章中我們通過create-react-app腳手架快速搭建了一個簡單的示例,並基於該示例講解了在類元件中React.Component和React.PureComponent背後的實現原理。同時我們也瞭解到,通過使用Babel預置工具包@babel/preset-react可以將類元件中ren

React16原始碼解讀開篇帶你搞懂幾個面試考點

引言 如今,主流的前端框架React,Vue和Angular在前端領域已成三足鼎立之勢,基於前端技術棧的發展現狀,大大小小的公司或多或少也會使用其中某一項或者多項技術棧,那麼掌握並熟練使用其中至少一種也成為了前端人員必不可少的技能飯碗。當然,框架的部分實現細節也常成為面試中的考察要點,因此,一方面為了應付面試

看過來~中國消協權威全新解讀揭祕電商法十大亮點!

電商法揭祕電商法十大亮點! 2018年8月31日,備受關注的《電子商務法》經第十三屆全國人大常委會第五次會議表決通過,並將於2019年1月1日起正式施行。在這部法律制定過程中,中國消費者協會和各地消費者協會(委員會,以下簡稱消協)廣泛聽取消費者意見,積極反映消費者訴求,認真開展研究工作,提出大量

React原始碼分析(一)-呼叫ReactDOM.render後發生了什麼

所謂知其然還要知其所以然. 本系列文章將分析 React 15-stable的部分原始碼, 包括元件初始渲染的過程、元件更新的過程等. 這篇文章先介紹元件初始渲染的過程的幾個重要概念, 包括大致過程、建立元素、例項化元件、事務、批量更新策略等. 在這之前, 假設讀者已經:

分散式快取技術redis學習系列(八)——JedisCluster原始碼解讀叢集初始化、slot(槽)的分配、值的存取

redis叢集環境,客戶端使用JedisCluster獲取連線並操作redis服務,上一篇 分散式快取技術redis學習系列(七)——spring整合jediscluster 簡單介紹了spring使用JedisCluster,這篇從JedisCluster原始

spring原始碼解讀BeanFactory介面

不知道為什麼看著Spring的原始碼,感觸最深的是Spring對概念的抽象,所以我就先學介面了,BeanFactory是Spring IOC實現的基礎,這邊定義了一系列的介面,我們通過這些介面的學習,可以大致瞭解BeanFactory體系各介面如何分工合作。 為學習具體實現

Caffe原始碼解讀Layer類

Layer類簡介 至少有一個輸入,輸出Blob。 部分Layer帶有權值和偏置項(如:啟用層沒有權值項) 前向傳播對輸入Blob處理,得到輸出Blob。反向傳播對輸出的diff進行處理,得到輸入的diff ProtoBuffer描述 開啟./caffe/src/caffe/caffe

Caffe原始碼解讀防止梯度爆炸的措施-梯度裁剪

 梯度裁剪是一種在非常深度的網路(通常是迴圈神經網路)中用於防止梯度爆炸(exploding gradient)的技術。 執行梯度裁剪的方法有很多,但常見的一種是當引數向量的 L2 範數(L2 norm)超過一個特定閾值時對引數向量的梯 度進行標準化,這個特定閾值根據函式:

Cocos Creator 原始碼解讀引擎啟動與主迴圈

# 前言 ## 預備 > 不知道你有沒有想過,假如把遊戲世界比作一輛汽車,那麼這輛“汽車”是如何啟動,又是如何持續運轉的呢? 如題,本文的內容主要為 Cocos Creator 引擎的**啟動流程**和**主迴圈**。 而在主迴圈的內容中還會涉及到:**元件的生命週期和計時器、緩動系統、動畫系統

Mybatis(四)MyBatis核心元件介紹原理解析和原始碼解讀 java中代理,靜態代理,動態代理以及spring aop代理方式,實現原理統一彙總

Mybatis核心成員 Configuration        MyBatis所有的配置資訊都儲存在Configuration物件之中,配置檔案中的大部分配置都會儲存到該類中 SqlSession         &

ORB-SLAM2原始碼解讀(2.2)單目初始化、勻速運動模型跟蹤、跟蹤參考關鍵幀、跟蹤區域性地圖

這裡是Tracking部分的第二部分,詳細講述各分支的程式碼及其實現原理。 單目初始化 MonocularInitialization() 目標:從初始的兩幀單目影象中,對SLAM系統進行初始化(得到初始兩幀的匹配,相機初始位姿,初始MapPoints),以便之後進行跟蹤。 方式

ORB-SLAM2原始碼解讀(2.1)Tracking

Tracking是SLAM的靈魂,更像是前端里程計VO,這裡Tracking的主要任務兩方面:(1)完成相機位姿估計(2)跟蹤區域性地圖 思路:TrackLocalMap()在當前幀和區域性地圖之間找到儘可能多的對應關係,優化當前幀的位姿。對每一幀都進行跟蹤 第一次接觸這麼大的工程,發現之前

ORB-SLAM2原始碼解讀(1)系統入口System

   先要拿大名鼎鼎的ORB-SLAM系統框圖鎮樓,看著這張圖能夠完美的串起來整個流程。 ORB-SLAM分三個執行緒,分別是Tracking、LocalMapping和LoopClosing。 (1)Tracking:在主執行緒上,輸入視訊流,輸出相機位姿並跟蹤區域

React 原始碼分析-呼叫ReactDOM.render後發生了什麼

我們知道, 對於一般的React 應用, 瀏覽器會首先執行程式碼 ReactDOM.render來渲染頂層元件, 在這個過程中遞迴渲染巢狀的子元件, 最終所有元件被插入到DOM中. 我們來看看 呼叫ReactDOM.render 發生了什麼 大致過程(只展示主要的函式呼叫): 1、

原始碼解讀(一)String類

曾聽過這麼一句話,美的東西看多了,自己創作的東西也會有所提高。我們的程式設計亦是如此,多看看大神的程式碼,欣賞他們的程式設計藝術,對我們的程式設計會有很大的幫助。而很多人經常忽略這一點,今天就讓ShowTime給大家送上第一道JDK大餐——解讀String類。 第一步看看String

【機器人學】機器人開源專案KDL原始碼學習(7)examples中的CMakeList.txt檔案解讀

通過學習KDL開源專案的程式碼可以學習CMake構建程式的知識,現簡單介紹一下orocos_kinematics_dynamics-master\orocos_kinematics_dynamics-master\orocos_kdl\examples\CMakeList.txt檔案的指令。

JDK原始碼解讀(第五彈Integer之toString方法)

上一篇只講了Integer的幾個屬性,這一次我們來看一下toString方法。 toString總共有3個過載,先來看兩個引數的toStirng方法: public static String toString(int i, int radix) {

【劉文彬】【原始碼解讀】EOS測試外掛txn_test_gen_plugin.cpp

原文連結:醒者呆的部落格園,https://www.cnblogs.com/Evsward/p/txn_test_gen_plugin.html 本文內容本屬於《【精解】EOS TPS 多維實測》的內容,但由於在編寫時篇幅過長,所以我決定將這一部分單獨成文撰寫,以便於理解。

Lumen開發lumen原始碼解讀之初始化(2)——門面(Facades)與資料庫(db)

緊接上一篇 $app->withFacades();//為應用程式註冊門面。 $app->withEloquent();//為應用程式載入功能強大的庫。 先來看看withFacades() /** * Register the facades

Lumen開發lumen原始碼解讀之初始化(1)——app例項

先來看看入口檔案public/index.php //請求頭 header('Content-Type: application/json; charset=utf-8'); /* |-------------------------------------------------