小前端讀源碼 - React16.7.0(二)

上一篇文章說到React代碼經過編譯後,會將JSX的語法都經過react.createElement函數轉換成一個對象傳入的ReactDOM.render中。本章將會接著閱讀ReactDOM.render中是如何將元素生成虛擬DOM以及如何渲染到頁面中的。

Lam:小前端讀源碼 - React16.7.0(一)?

zhuanlan.zhihu.com圖標

接著上一章說到的,去看看ReactDOM裡面到底有什麼。從源碼當中我們發現ReactDOM提供了一些屬性和方法。其中的作用自行查文檔了。

  • createPortal
  • findDOMNode
  • hydrate
  • render
  • unstable_renderSubtreeIntoContainer
  • unmountComponentAtNode
  • unstable_createPortal
  • unstable_batchedUpdates
  • unstable_interactiveUpdates
  • flushSync
  • unstable_createRoot -> React 17版本將會廢除
  • unstable_flushControlled
  • __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

我們經常使用的一般都是ReactDOM.render這個API,將我們的組件渲染到頁面中,我們就一起看看render裡面到底做了什麼事情吧!

首先render接受3個參數,element、container、callback。從React16版本開始,element就是我們經過react.createElement後返回的對象。container就是我們需要渲染到的元素。render將不會return會組件對象了,改為在callback中返回。在render中會將數據傳入一個叫做legacyRenderSubtreeIntoContainer的方法中。

legacyRenderSubtreeIntoContainer

首先legacyRenderSubtreeIntoContainer會檢查傳入的container的類型,如果傳入的類型不符合規定將會報錯。那麼可以傳入什麼類型呢?

function isValidContainer(node) {
return !!(node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE || node.nodeType === COMMENT_NODE && node.nodeValue === react-mount-point-unstable ));
}

react會檢測傳入的container的nodeType是需要等於1、9、11或者8,如果nodeType等於8的情況下,還需要nodeValue必須等於" react-mount-point-unstable "。

並且如果controller是body的話,也會出現waring提示!

!(container.nodeType !== ELEMENT_NODE || !container.tagName || container.tagName.toUpperCase() !== BODY) ? warningWithoutStack$1(false, render(): Rendering components directly into document.body is + discouraged, since its children are often manipulated by third-party + scripts and browser extensions. This may lead to subtle + reconciliation issues. Try rendering into a container element created + for your app.) : void 0;

接著react會判斷你當前傳入的contrainer是不是已經是一個react的根組件,會通過判斷傳入的contrainer中是否存在_reactRootContainer這個對象進行判斷,react會在渲染的同時將_reactRootContainer注入到contrainer對象中。

並且會根據是否存在_reactRootContainer進行不同的渲染方式,我們先看當前的contrainer是沒有渲染過任何reactElement的情況下如何執行的。

接著在legacyRenderSubtreeIntoContainer函數中會執行幾個比較重要的函數將傳入的container和children以及children中的children一一渲染的函數了,最終將組件對象返回的一系列操作。

  1. legacyCreateRootFromDOMContainer
  2. unbatchedUpdates
  3. render / legacy_renderSubtreeIntoContainer

legacyCreateRootFromDOMContainer

如果當前傳入的container並不是一個已經初始化的容器,那麼將會執行legacyCreateRootFromDOMContainer這個函數,那麼從源碼也看到了,在執行完legacyCreateRootFromDOMContainer之後其實就會將返回的值賦值到container中的_reactRootContainer了。如果之後還對同一個container進行render的話,就會判斷到存在_reactRootContainer這一個對象,那麼就會進入這個判斷中了。那麼legacyCreateRootFromDOMContainer到底幫我們做了什麼事情呢?我們先看看它返回了什麼東西回來。

好吧,我們並不知道裡面的是什麼,那麼只能看看legacyCreateRootFromDOMContainer函數裡面執行了什麼東西了。

首先react需要判斷你是不是伺服器渲染,其實早在ReactDOM對象內到legacyCreateRootFromDOMContainer之間有很多關於伺服器渲染的判斷,但是我們現在目標是先搞懂react在瀏覽器渲染的流程和邏輯,所以我們會先跳過一些伺服器渲染的流程和邏輯。那麼legacyCreateRootFromDOMContainer一開始就會通過傳入的forceHydrateshouldHydrateDueToLegacyHeuristic去判斷是不是伺服器渲染,那麼結果當然是false啦。那麼就會進入到一個清楚container內容的判斷中。

清楚內容的邏輯是先獲取到container的lastChild,然後判斷lastChild是否為一個元素,並且這個元素不能帶有data-reactroot這個屬性,否則報錯。然後刪除掉這個子元素,這是一個循環直到container的lastChild為null才會停止。

有時候我們需要在react.js和業務js載入前出現一些占點陣圖或者loading圖片這一些提高首屏的方式,那麼就無可避免的在contrainer裡面寫入一些默認的html標籤去實現佔位樣式了。

<div id="root">
<p>佔位</p>
<p>佔位</p>
<p>佔位</p>
...
</div>

這樣就會導致在legacyCreateRootFromDOMContainer中需要刪除container內的子元素要循環多次,所以一個優化的點就是把裡面同級的內容包在一個元素中,那麼只需要循環一次就可以了。

<div id="root">
<div>
<p>佔位</p>
<p>佔位</p>
<p>佔位</p>
...
</div>
</div>

這裡其實也只是判斷是否為伺服器渲染。

最後就將參數傳入ReactRoot並實例化ReactRoot後返回。

ReactRoot

從源碼看到ReactRoot這個構造函數就是通過一系列的函數初始化了一堆屬性(應該是屬於狀態之類的變數)。然後賦值到this._internalRoot中。執行的函數順序如下:

  1. createContainer
  2. createFiberRoot

因為react在16.2就已經修改為了Fiber架構,所以這裡createFiberRoot只是其中一個創建Fiber一種方式而已。

暫時我們先用到的是createHostRootFiber這個函數。所有的fiber都是FiberNode的實例。

最終輸出的就是一開始我們看見的那個對象。

當然ReactRoot的原型上有以下4個API:

  • render
  • unmount
  • legacy_renderSubtreeIntoContainer
  • createBatch

我們常用的估計也就render和unmount這兩個了。而legacy_renderSubtreeIntoContainer和createBatch這兩個API在文檔中其實也沒有說明。

到這位置其實就是整個container的_reactRootContainer初始化過程了,那麼我們就回到legacyRenderSubtreeIntoContainer這個函數中繼續往下看渲染過程了。

legacyRenderSubtreeIntoContainer會對我們ReactDOM.render傳入的第三個參數(回到函數)進行一個包裝。最終返回的是this._internalRoot.current.child.stateNode。

接著就是一個批處理的判斷,但是還沒有發現這個批處理是什麼情況會使用,我們先忽略它。

到這裡為止,其實都是創建關鍵的root根對象。接下來就是root.render將要渲染到根對象中的App的ReactElement對象進行一些操作了。

root.render

需要關注的是在render函數內有2個地方是需要注意的:

  • ReactWork
  • updateContainer

ReactWork是什麼東西呢?

其實ReactWork是一個很簡單的東西,它有兩個值_callbacks和_didCommit。通過執行then函數傳入callback,如果判斷到當前的_didCommit為false的情況下,就將callback添加到_callbacks數組內。然後通過執行_onCommit去改變_didCommit的值,之後循環執行_callbacks中的callback。

updateContainer

render函數之後會執行updateContainer函數,傳入children,root和work實例化後的_onCommit函數。因為這個render其實是root根對象上的render,所以children就是App(當然也可以是其他,視乎你執行ReactDOM.render時傳入的第一個參數是什麼)。

在updateContainer中會通過requestCurrentTime和computeExpirationForFiber得出currentTime和expirationTime這個兩個時間之後傳入到updateContainerAtExpirationTime中,之後再傳入到scheduleRootUpdate中。

scheduleRootUpdate會將expirationTime傳入一個createUpdate函數中創建一個update對象。並且將element賦值到update.payload中(element就是App的ReactElement),並且將callback賦值到update.callback中。

接著會執行enqueueUpdate函數,這個函數其實大概的意思就是將新建的update對象和當前的FiberNode對象傳入,然後為current$$1這個對象添加了updateQueue對象,裡面保存著相關的一些任務。以下是返回結果:

之後就執行scheduleWork函數。曾經斷點開過這個函數執行完之後,頁面就會渲染出dom節點了並且回調函數也執行了。無比興奮。

其實到源碼看到這裡發現很多問題,例如react很喜歡用全局變數,而且裡面發現其實為了之後的非同步渲染做了不少準備的,很多的判斷代碼。開始有點懷疑是不是應該讀16.7.0版本的代碼,但是已經開始了,那就繼續吧。

總結

整個流程是比較複雜,中間很多對象之間的引用,又實例一些對象,如果單看上面的流程比較懵逼的話,沒有關係,我在這裡梳理一下整個流程,最終傳入scheduleWork前的參數是怎麼生成出來的,原來的container和children去哪裡了呢?我們通過一個流程圖去說明整個流程是怎樣的。

下一篇繼續說如何渲染到真實DOM當中的。

推薦閱讀:

TAG:React | 前端框架 | JavaScript |