How React Works (一)首次渲染

1、前言

     本文將會經過一個簡單的例子,結合React源碼(v 16.4.2)來講明 React 是如何工做的,而且幫助讀者理解 ReactElement、Fiber 之間的關係,以及 Fiber 在各個流程的做用。看完這篇文章有助於幫助你更加容易地讀懂 React 源碼。初期計劃有如下幾篇文章:css

  1. 首次渲染
  2. 事件機制
  3. 更新流程
  4. 調度機制

2、核心類型解析

     在正式進入流程講解以前,先了解一下 React 源碼內部的核心類型,有助於幫助咱們更好地瞭解整個流程。爲了讓你們更加容易理解,後續的描述只抽取核心部分,把 ref、context、異步、調度、異常處理 之類的簡化掉了。   react

1. ReactElement

  咱們寫 React 組件的時候,一般會使用JSX來描述組件。<p></p>這種寫法通過babel轉換後,會變成以 React.createElement(type, props, children)形式。而咱們的例子中,type會是兩種類型:functionstringfunction通常是指ReactComponentconstructor或者函數式組件的function,而string類型的就是HTML標籤。算法

  這個方法,最終是會返回一個 ReactElement ,他是一個普通的 Object ,不是經過某個 class 實例化而來的,大概看看便可,核心成員以下:數組

key type desc
$$typeof Symbol|Number 對象類型標識,用於判斷當前Object是否一個某種類型的ReactElement
type Function|String|Symbol|Number|Object 若是當前ReactElement是一個ReactComponent,那這裏將是它對應的Constructor;而普通HTML標籤,通常都是String
props Object ReactElement上的全部屬性,包含children這個特殊屬性

2. ReactRoot

  當前放在ReactDom.js內部,能夠理解爲React渲染的入口。咱們調用ReactDom.render以後,核心就是建立一個 ReactRoot ,而後調用 ReactRoot 實例的render方法,進入渲染流程的。bash

key type desc
render Function 渲染入口方法
_internalRoot FiberRoot 根據當前DOMContainer建立的一個FiberTree的根

3. FiberRoot

  FiberRoot 是一個 Object ,是後續初始化、更新的核心根對象。核心成員以下:babel

key type desc
current (HostRoot)FiberNode 指向當前已經完成的Fiber Tree 的Root
containerInfo DOMContainer React的DOM容器,把整個React渲染到這個DOM內部
finishedWork (HostRoot)FiberNode|null 指向當前已經完成準備工做的Fiber Tree Root

current、finishedWork,都是一個(HostRoot)FiberNode,究竟是爲何呢?先賣個關子,後面將會講解。數據結構

4. FiberNode

  在 React 16以後,Fiber Reconciler 就做爲 React 的默認調度器,核心數據結構就是由FiberNode組成的 Node Tree 。先參觀下他的核心成員:app

key type desc
實例相關 --- ---
tag Number FiberNode的類型,能夠在packages/shared/ReactTypeOfWork.js中找到。當前文章 demo 能夠看到ClassComponent、HostRoot、HostComponent、HostText這幾種
type Function|String|Symbol|Number|Object 和ReactElement表現一致
stateNode FiberRoot|DomElement|ReactComponentInstance FiberNode會經過stateNode綁定一些其餘的對象,例如FiberNode對應的Dom、FiberRoot、ReactComponent實例
Fiber遍歷流程相關
return FiberNode|null 表示父級 FiberNode
child FiberNode|null 表示第一個子 FiberNode
sibling FiberNode|null 表示牢牢相鄰的下一個兄弟 FiberNode
alternate FiberNode|null Fiber調度算法採起了雙緩衝池算法,FiberRoot底下的全部節點,都會在算法過程當中,嘗試建立本身的「鏡像」,後面將會繼續講解
數據相關
pendingProps Object 表示新的props
memoizedProps Object 表示通過全部流程處理後的新props
memoizedState Object 表示通過全部流程處理後的新state
反作用描述相關
updateQueue UpdateQueue 更新隊列,隊列內放着即將要發生的變動狀態,詳細內容後面再講解
effectTag Number 16進制的數字,能夠理解爲經過一個字段標識n個動做,如Placement、Update、Deletion、Callback……因此源碼中看到不少 &=
firstEffect FiberNode|null 與反作用操做遍歷流程相關 當前節點下,第一個須要處理的反作用FiberNode的引用
nextEffect FiberNode|null 表示下一個將要處理的反作用FiberNode的引用
lastEffect FiberNode|null 表示最後一個將要處理的反作用FiberNode的引用

5. Update

  在調度算法執行過程當中,會將須要進行變動的動做以一個Update數據來表示。同一個隊列中的Update,會經過next屬性串聯起來,實際上也就是一個單鏈表。框架

key type desc
tag Number 當前有0~3,分別是UpdateState、ReplaceState、ForceUpdate、CaptureUpdate
payload Function|Object 表示這個更新對應的數據內容
callback Function 表示更新後的回調函數,若是這個回調有值,就會在UpdateQueue的反作用鏈表中掛在當前Update對象
next Update UpdateQueue中的Update之間經過next來串聯,表示下一個Update對象

6. UpdateQueue

  在 FiberNode 節點中表示當前節點更新的反作用(主要是Callback)的集合,下面的結構省略了CapturedUpdate部分dom

key type desc
baseState Object 表示更新前的基礎狀態
firstUpdate Update 第一個 Update 對象引用,整體是一條單鏈表
lastUpdate Update 最後一個 Update 對象引用
firstEffect Update 第一個包含反作用(Callback)的 Update 對象的引用
lastEffect Update 最後一個包含反作用(Callback)的 Update 對象的引用

3、代碼樣例

  本次流程說明,使用下面的源碼進行分析

//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.js
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      msg:'init',
    };
  }
  render() {
    return (
      <div className="App">
        <p className="App-intro">
          To get started, edit <code>{this.state.msg}</code> and save to reload.
        </p>
        <button onClick={() => {
          this.setState({msg: 'clicked'});
        }}>hehe
        </button>
      </div>
    );
  }

}

export default App;
複製代碼

4、渲染調度算法 - 準備階段

  從ReactDom.render方法開始,正式進入渲染的準備階段。

1. 初始化基本節點

  建立 ReactRoot、FiberRoot、(HostRoot)FiberNode,創建他們與 DOMContainer 的關係。

2. 初始化(HostRoot)FiberNodeUpdateQueue

  經過調用ReactRoot.render,而後進入packages/react-reconciler/src/ReactFiberReconciler.jsupdateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate一系列方法調用,爲此次初始化建立一個Update,把<App />這個 ReactElement 做爲 Update 的payload.element的值,而後把 Update 放到 (HostRoot)FiberNode 的 updateQueue 中。

而後調用scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot,期間主要是提取當前應該進行初始化的 (HostFiber)FiberNode,後續正式進入算法執行階段。

5、渲染調度算法 - 執行階段

  因爲本次是初始化,因此須要調用packages/react-reconciler/src/ReactFiberScheduler.jsrenderRoot方法,生成一棵完整的FiberNode Tree finishedWork

1. 生成 (HostRoot)FiberNode 的workInProgress,即current.alternate

  在整個算法過程當中,主要作的事情是遍歷 FiberNode 節點。算法中有兩個角色,一是表示當前節點原始形態的current節點,另外一個是表示基於當前節點進行從新計算的workInProgress/alternate節點。兩個對象實例是獨立的,相互以前經過alternate屬性相互引用。對象的不少屬性都是先複製再重建的。

第一次建立結果示意圖:

  這個作法的核心思想是雙緩池技術(double buffering pooling technique),由於須要作 diff 的話,起碼是要有兩棵樹進行對比。經過這種方式,能夠把樹的整體數量限制在2,節點、節點屬性都是延遲建立的,最大限度地避免內存使用量因算法過程而不斷增加。後面的更新流程的文章裏,會了解到這個雙緩衝怎麼玩。

2. 工做執行循環

示意代碼以下:

nextUnitOfWork = createWorkInProgress(
  nextRoot.current,
  null,
  nextRenderExpirationTime,
);
....

while (nextUnitOfWork !== null) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
複製代碼

剛剛建立的 FiberNode 被做爲nextUnitOfWork,今後進入工做循環。從上面的代碼能夠看出,在是一個典型的遞歸的循環寫法。這樣寫成循環,一來就是和傳統的遞歸改循環寫法同樣,避免調用棧不斷堆疊以及調用棧溢出等問題;二來在結合其餘Scheduler代碼的輔助變量,能夠實現遍歷隨時終止、隨時恢復的效果。

咱們繼續深刻performUnitOfWork函數,能夠看到相似的代碼框架:

const current = workInProgress.alternate;
//...
next = beginWork(current, workInProgress, nextRenderExpirationTime);
//...
if (next === null) {
    next = completeUnitOfWork(workInProgress);
}
//...
return next;
複製代碼

從這裏能夠看出,這裏對 workInProgress 節點進行一些處理,而後會經過必定的遍歷規則返回next,若是next不爲空,就返回進入下一個performUnitOfWork,不然就進入completeUnitOfWork

3. beginWork

  每一個工做的對象主要是處理workInProgress。這裏經過workInProgress.tag區分出當前 FiberNode 的類型,而後進行對應的更新處理。下面介紹咱們例子裏面能夠遇到的兩種處理比較複雜的 FiberNode 類型的處理過程,而後再單獨講解裏面比較重要的processUpdateQueue以及reconcileChildren過程。

3.1 HostRoot - updateHostRoot

  HostRoot,即文中常常講到的 (HostRoot)FiberNode,表示它是一個 HostRoot 類型的 FiberNode ,代碼中經過FiberRoot.tag表示。

  前面講到,在最開始初始化的時候,(HostRoot)FiberNode 在初始化以後,初始化了他的updateQueue,裏面放了準備處理的子節點。這裏就作兩個動做:

  • 處理更新隊列,得出新的state - processUpdateQueue方法
  • 建立或者更新 FiberNode 的child,獲得下一個工做循環的入參(也是FiberNode) - ChildReconciler方法

  經過這兩個函數的詳細內容屬於比較通用的部分,將在後面單獨講解。

3.2 ClassComponent - updateClassComponent

  ClassComponent,即咱們在寫 React 代碼的時候本身寫的 Component,即例子中的App

3.2.1 建立ReactComponent實例階段

  對於還沒有初始化的節點,這個方法主要是經過FiberNode.type這個 ReactComponent Constructor 來建立 ReactComponent 實例並建立與 FiberNode 的關係。

(ClassComponent)FiberNode 與 ReactComponent 的關係示意圖:

  初始化後,會進入實例的mount過程,即把 Component render以前的週期方法都調用完。期間,state可能會被如下流程修改:

  • 調用getDerivedStateFromProps
  • 調用componentWillMount -- deprecated
  • 處理因上面的流程產生的Update所調用的processUpdateQueue
3.2.2 完成階段 - 建立 child FiberNode

  在上面初始化Component實例以後,經過調用實例的render獲取子 ReactElement,而後建立對應的全部子 FiberNode 。最終將workInProgress.child指向第一個子 FiberNode。

3.4 處理節點的更新隊列 - processUpdateQueue 方法

  在解釋流程以前,先回顧一下updateQueue的數據結構:

  從上面的結構能夠看出,UpdateQueue 是存放整個 Update 單向鏈表的容器。裏面的 baseState 表示更新前的原始 State,而經過遍歷各個 Update 鏈表後,最終會獲得一個新的 baseState。

  對於單個 Update 的處理,主要是根據Update.tag來進行區分處理。

  • ReplaceState:直接返回這裏的 payload。若是 payload 是函數,則使用它的返回值做爲新的 State。
  • CaptureUpdate:僅僅是將workInProgress.effectTag設置爲清空ShouldCapture標記位,增長DidCapture標記位。
  • UpdateState:若是payload是普通對象,則把他當作新 State。若是 payload 是函數,則把執行函數獲得的返回值做爲新 State。若是新 State 不爲空,則與原來的 State 進行合併,返回一個新對象
  • ForceUpdate:僅僅是設置 hasForceUpdate爲 true,返回原始的 State。

  總體而言,這個方法要作的事情,就是遍歷這個 UpdateQueue ,而後計算出最後的新 State,而後存到workInProgress.memoizedState中。

3.5 處理子FiberNode - reconcileChildren 方法

  在 workInProgress 節點自身處理完成以後,會經過props.children或者instance.render方法獲取子 ReactElement。子 ReactElement 多是對象數組字符串迭代器,針對不一樣的類型進行處理。

  • 下面經過 ClassComponent 及其 數組類型 child的場景來說解子 FiberNode 的建立、關聯流程(reconcileChildrenArray方法):

  在頁面初始化階段,因爲沒有老節點的存在,流程上就略過了位置索引比對、兄弟元素清理等邏輯,因此這個流程相對簡單。

  遍歷以前render方法生成的 ReactElement 數組,一一對應地生成 FiberNode。FiberNode 有returnFiber屬性和sibling屬性,分別指向其父親 FiberNode和緊鄰的下一個兄弟 FiberNode。這個數據結構和後續的遍歷過程相關。

  如今,生成的FiberNode Tree 結構以下:

  圖中的兩個(HostComponent)FiberNode就是剛剛生成的子 FiberNode,即源碼中的<p>...</p><button>...</button>。這個方法最後返回的,是第一個子 FiberNode,就經過這種方式建立了(ClassComponent)FiberNode.child與第一個子 FiberNode的關係。

  這個時候,再搬出剛剛曾經看過的代碼:

const current = workInProgress.alternate;
//...
next = beginWork(current, workInProgress, nextRenderExpirationTime);
//...
if (next === null) {
    next = completeUnitOfWork(workInProgress);
}
//...
return next;
複製代碼

  意味着剛剛返回的 child 會被當作 next 進入下一個工做循環。如此往復,會獲得下面這樣的 FiberNode Tree :

  生成這棵樹以後,被返回的是左下角的那個 (HostText)FiberNode。而從新進入beginWork方法後,因爲這個 FiberNode 並無 child ,根據上面的代碼邏輯,會進入completeUnitOfWork方法。

注意:雖說本例子的 FiberNode Tree 最終形態是這樣子的,但實際上算法是優先深度遍歷,到葉子節點以後再遍歷緊鄰的兄弟節點。若是兄弟節點有子節點,則會繼續擴展下去。

4. completeUnitOfWork

  進入這個流程,代表 workInProgress 節點是一個葉子節點,或者它的子節點都已經處理完成了。如今開始要完成這個節點處理的剩餘工做。   

4.1 建立DomElement,處理子DomElement 綁定關係

completeWork方法中,會根據workInProgress.tag來區分出不一樣的動做,下面挑選2個比較重要的來進一步分析:

4.1.1 HostText

  此前提到過,FiberNode.stateNode能夠用於存放 DomElement Instance。在初始化過程當中,stateNode 爲 null,因此會經過document.createTextNode建立一個 Text DomElement,節點內容就是workInProgress.memoizedProps。最後,經過__reactInternalInstance$[randomKey]屬性創建與本身的 FiberNode的聯繫。

4.1.2 HostComponent

  在本例子中,處理完上面的 HostText 以後,調度算法會尋找當前節點的 sibling 節點進行處理,因此進入了HostComponent的處理流程。

  因爲當前出於初始化流程,因此處理比較簡單,只是根據FiberNode.tag(當前值是code)來建立一個 DomElement,即經過document.createElement來建立節點。而後經過__reactInternalInstance$[randomKey]屬性創建與本身的 FiberNode的聯繫;經過__reactEventHandlers$[randomKey]來創建與 props 的聯繫。

  完成 DomElement 自身的建立以後,若是有子節點,則會將子節點 append 到當前節點中。如今先略過這個步驟。

  後續,經過setInitialProperties方法對 DomElement 的屬性進行初始化,而<code>節點的內容、樣式、class、事件 Handler等等也是這個時候存放進去的。

  如今,整個 FiberNode Tree 以下:

  通過屢次循環處理,得出如下的 FiberNode Tree:

  以後,回到紅色箭頭指向的 (HostComponent)FiberNode,能夠分析一下以前省略掉的子節點處理流程。

  在當前 DomElement 建立完畢後,進入appendAllChildren方法把子節點 append 到當前 DomElement 。由上面的流程能夠知道,能夠經過 workInProgress.child -> workInProgress.child.sibling -> workInProgress.child.sibling.sibling ....找到全部子節點,而每一個節點的 stateNode 就是對應的 DomElement,因此經過這種方式的遍歷,就能夠把全部的 DomElement 掛載到 父 DomElement中。

  最終,和 DomElement 相關的 FiberNode 都被處理完,得出下面的FiberNode 全貌:

4.2 將當前節點的 effect 掛在到 returnFiber 的 effect 末尾

  在前面講解基礎數據結構的時候描述過,每一個 FiberNode 上都有 firstEffect、lastEffect ,指向一個Effect(反作用) FiberNode鏈表。在處理完當前節點,即將返回父節點的時候,把當前的鏈條掛接到 returnFiber 上。最終,在(HostRoot)FiberNode.firstEffect 上掛載着一條擁有當前 FiberNode Tree 全部反作用的 FiberNode 鏈表。

5. 執行階段結束

  經歷完以前的全部流程,最終 (HostRoot)FiberNode 也被處理完成,就把 (HostRoot)FiberNode 返回,最終做爲finishedWork返回到 performWorkOnRoot,後續進入下一個階段。

6、渲染調度算法 - 提交階段

  所謂提交階段,就是實際執行一些周期函數、Dom 操做的階段。

  這裏也是一個鏈表的遍歷,而遍歷的就是以前階段生成的 effect 鏈表。在遍歷以前,因爲初始化的時候,因爲 (HostRoot)FiberNode.effectTagCallback(初始化回調)),會先將 finishedWork 放到鏈表尾部。結構以下:

每一個部分提交完成以後,都會把遍歷節點重置到finishedWork.firstEffect

1. 提交節點裝載( mount )前的操做

  當前這個流程處理的只有屬於 ReactComponent 的 getSnapshotBeforeUpdate方法。   

2. 提交端原生節點( Host )的反作用(插入、修改、刪除)

  遍歷到某個節點後,會根據節點的 effectTag 決定進行什麼操做,操做包括插入( Placement )修改( Update )刪除( Deletion )

  因爲當前是首次渲染,因此會進入插入( Placement )流程,其他流程將在後面的《How React Works(三)更新流程》中講解。

2.1 插入流程( Placement )

  要作插入操做,必先找到兩個要素:父親 DomElement ,子 DomElement。

2.1.1 找到相對於當前 FiberNode 最近的父親 DomElement

  經過FiberNode.return不斷往上找,找到最近的(HostComponent)FiberNode、(HostRoot)FiberNode、(HostPortal)FiberNode節點,而後經過(HostComponent)FiberNode.stateNode(HostRoot)FiberNode.stateNode.containerInfo(HostPortal)FiberNode.stateNode.containerInfo就能夠獲取到對應的 DomElement 實例。   

2.1.2 找到相對於當前 FiberNode 最近的全部遊離子 DomElement

  實際上,把目標是查找當前 FiberNode底下全部鄰近的 (HostComponent)FiberNode、(HostText)FiberNode,而後經過 stateNode 屬性就能夠獲取到待插入的 子DomElement 。

  所謂全部鄰近的,能夠經過這幅圖來理解:

  圖中紅框部分FiberNode.stateNode,就是要被添加到父親 DomElement的 子 DomElement。

  遍歷順序,和以前的生成 FiberNode Tree時順序大體相同:

a) 訪問child節點,直至找到 FiberNode.type 爲 HostComponent 或者 HostRoot 的節點,獲取到對應的 stateNode ,append 到 父 DomElement中。

b) 尋找兄弟節點,若是有,就訪問兄弟節點,返回 a) 。

c) 若是沒有兄弟節點,則訪問 return 節點,若是 return 不是當前算法入參的根節點,就返回a)。

d) 若是 return 到根節點,則退出。

3. 改變 workInProgress/alternate/finishedWork 的身份

  雖然是短短的一行代碼,但這個十分重要,因此單獨標記:

root.current = finishedWork;
複製代碼

  這意味着,在 DomElement 反作用處理完畢以後,意味着以前講的緩衝樹已經完成任務,翻身當主人,成爲下次修改過程的current。再來看一個全貌:

4. 提交裝載、變動後的生命週期調用操做

  在這個流程中,也是遍歷 effect 鏈表,對於每種類型的節點,會作不一樣的處理。

4.1 ClassComponent

  若是當前節點的 effectTag 有 Update 的標誌位,則須要執行對應實例的生命週期方法。在初始化階段,因爲當前的 Component 是第一次渲染,因此應該執行componentDidMount,其餘狀況下應該執行componentDidUpdate

  以前講到,updateQueue 裏面也有 effect 鏈表。裏面存放的就是以前各個 Update 的 callback,一般就來源於setState的第二個參數,或者是ReactDom.rendercallback。在執行完上面的生命週期函數後,就開始遍歷這個 effect 鏈表,把 callback 都執行一次。

4.2 HostRoot

  操做和 ClassComponent 處理的第二部分一致。

4.3 HostComponent

  這部分主要是處理初次加載的 HostComponent 的獲取焦點問題,若是組件有autoFocus這個 props ,就會獲取焦點。      

7、小結

  本文主要講述了ReactDom.render的內部的工做流程,描述了 React 初次渲染的內在流程:

  1. 建立基礎對象: ReactRoot、FiberRoot、(HostRoot)FiberNode
  2. 建立 HostRoot 的鏡像,經過鏡像對象來作初始化
  3. 初始化過程,經過 ReactElement 引導 FiberNode Tree 的建立
  4. 父子 FiberNode 經過childreturn鏈接
  5. 兄弟 FiberNode 經過sibling鏈接
  6. FiberNode Tree 建立過程,深度優先,到底以後建立兄弟節點
  7. 一旦到達葉子節點,就開始建立 FiberNode 對應的 實例,例如對應的 DomElement 實例、ReactComponent 實例,並將實例經過FiberNode.stateNode建立關聯。
  8. 若是當前建立的是 ReactComponent 實例,則會調用調用getDerivedStateFromPropscomponentWillMount方法
  9. DomElement 建立以後,若是 FiberNode 子節點中有建立好的 DomElement,就立刻 append 到新建立的 DomElement 中
  10. 構建完成整個FiberNode Tree 後,對應的 DomElement Tree 也建立好了,後續進入提交過程
  11. 在建立 DomElement Tree 的過程當中,同時會把當前的反作用不斷往上傳遞,在提交階段裏面,會找到這種標記,並把剛建立完的 DomElement Tree 裝載到容器 DomElement中
  12. 雙緩衝的兩棵樹 FiberNode Tree 角色互換,原來的 workInProgress 轉正
  13. 執行對應 ReactComponent 的裝載後生命週期方法componentDidMount
  14. 其餘回調調用、autoFocus 處理

 下一篇文章將會描述 React 的事件機制(但聽說準備要重構),但願我不會斷耕。

寫完第一篇,React 版本已經到了 16.5.0 ……

相關文章
相關標籤/搜索