本文將會經過一個簡單的例子,結合React源碼(v 16.4.2)來講明 React 是如何工做的,而且幫助讀者理解 ReactElement、Fiber 之間的關係,以及 Fiber 在各個流程的做用。看完這篇文章有助於幫助你更加容易地讀懂 React 源碼。初期計劃有如下幾篇文章:css
在正式進入流程講解以前,先了解一下 React 源碼內部的核心類型,有助於幫助咱們更好地瞭解整個流程。爲了讓你們更加容易理解,後續的描述只抽取核心部分,把 ref、context、異步、調度、異常處理 之類的簡化掉了。
node
咱們寫 React 組件的時候,一般會使用JSX
來描述組件。<p></p>
這種寫法通過babel轉換後,會變成以 React.createElement(type, props, children)形式。而咱們的例子中,type
會是兩種類型:function
、string
,實際上就是App
的constructor
方法,以及其餘HTML
標籤。react
而這個方法,最終是會返回一個 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這個特殊屬性 |
當前放在ReactDom.js內部,能夠理解爲React渲染的入口。咱們調用ReactDom.render
以後,核心就是建立一個 ReactRoot ,而後調用 ReactRoot 實例的render
方法,進入渲染流程的。數組
key | type | desc |
---|---|---|
render | Function | 渲染入口方法 |
_internalRoot | FiberRoot | 根據當前DomContainer建立的一個FiberTree的根 |
FiberRoot 是一個 Object ,是後續初始化、更新的核心根對象。核心成員以下:babel
key | type | desc |
---|---|---|
current | (HostRoot)FiberNode | 指向當前已經完成的Fiber Tree 的Root |
containerInfo | DomContainer | 根據當前DomContainer建立的一個FiberTree的根 |
finishedWork | (HostRoot)FiberNode|null | 指向當前已經完成準備工做的Fiber Tree Root |
current、finishedWork,都是一個(HostRoot)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的引用 |
在調度算法執行過程當中,會將須要進行變動的動做以一個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對象 |
在 FiberNode 節點中表示當前節點更新、更新的反作用(主要是Callback)的集合,下面的結構省略了CapturedUpdate部分dom
key | type | desc |
---|---|---|
baseState | Object | 表示更新前的基礎狀態 |
firstUpdate | Update | 第一個 Update 對象引用,整體是一條單鏈表 |
lastUpdate | Update | 最後一個 Update 對象引用 |
firstEffect | Update | 第一個包含反作用(Callback)的 Update 對象的引用 |
lastEffect | Update | 最後一個包含反作用(Callback)的 Update 對象的引用 |
本次流程說明,使用下面的源碼進行分析
//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;
從ReactDom.render
方法開始,正式進入渲染的準備階段。
建立 ReactRoot、FiberRoot、(HostRoot)FiberNode,創建他們與 DomContainer 的關係。
(HostRoot)FiberNode
的UpdateQueue
經過調用ReactRoot.render
,而後進入packages/react-reconciler/src/ReactFiberReconciler.js
的updateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate
一系列方法調用,爲此次初始化建立一個Update,把<App />
這個 ReactElement 做爲 Update 的payload.element
的值,而後把 Update 放到 (HostRoot)FiberNode 的 updateQueue 中。
而後調用scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot
,期間主要是提取當前應該進行初始化的 (HostFiber)FiberNode,後續正式進入算法執行階段。
因爲本次是初始化,因此須要調用packages/react-reconciler/src/ReactFiberScheduler.js
的renderRoot
方法,生成一棵完整的FiberNode Tree finishedWork
。
workInProgress
,即current.alternate
。 在整個算法過程當中,主要作的事情是遍歷 FiberNode 節點。算法中有兩個角色,一是表示當前節點原始形態的current
節點,另外一個是表示基於當前節點進行從新計算的workInProgress/alternate
節點。兩個對象實例是獨立的,相互以前經過alternate
屬性相互引用。對象的不少屬性都是先複製再重建
的。
第一次建立結果示意圖:
這個作法的核心思想是雙緩池技術(double buffering pooling technique)
,由於須要作 diff 的話,起碼是要有兩棵樹進行對比。經過這種方式,能夠把樹的整體數量限制在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
。
每一個工做的對象主要是處理workInProgress
。這裏經過workInProgress.tag
區分出當前 FiberNode 的類型,而後進行對應的更新處理。下面介紹咱們例子裏面能夠遇到的兩種處理比較複雜的 FiberNode 類型的處理過程,而後再單獨講解裏面比較重要的processUpdateQueue
以及reconcileChildren
過程。
HostRoot,即文中常常講到的 (HostRoot)FiberNode,表示它是一個 HostRoot 類型的 FiberNode ,代碼中經過FiberRoot.tag
表示。
前面講到,在最開始初始化的時候,(HostRoot)FiberNode 在初始化以後,初始化了他的updateQueue
,裏面放了準備處理的子節點。這裏就作兩個動做:
child
,獲得下一個工做循環的入參(也是FiberNode) - ChildReconciler方法經過這兩個函數的詳細內容屬於比較通用的部分,將在後面單獨講解。
ClassComponent,即咱們在寫 React 代碼的時候本身寫的 Component,即例子中的App
。
ReactComponent
實例階段 對於還沒有初始化的節點,這個方法主要是經過FiberNode.type
這個 ReactComponent Constructor 來建立 ReactComponent 實例並建立與 FiberNode 的關係。
(ClassComponent)FiberNode 與 ReactComponent 的關係示意圖:
初始化後,會進入實例的mount
過程,即把 Component render
以前的週期方法都調用完。期間,state
可能會被如下流程修改:
在上面初始化Component實例以後,經過調用實例的render
獲取子 ReactElement,而後建立對應的全部子 FiberNode 。最終將workInProgress.child
指向第一個子 FiberNode。
在解釋流程以前,先回顧一下updateQueue的數據結構:
從上面的結構能夠看出,UpdateQueue 是存放整個 Update 單向鏈表的容器。裏面的 baseState 表示更新前的原始 State,而經過遍歷各個 Update 鏈表後,最終會獲得一個新的 baseState。
對於單個 Update 的處理,主要是根據Update.tag
來進行區分處理。
workInProgress.effectTag
設置爲清空ShouldCapture
標記位,增長DidCapture
標記位。新對象
。hasForceUpdate
爲 true,返回原始的 State。 總體而言,這個方法要作的事情,就是遍歷這個 UpdateQueue ,而後計算出最後的新 State,而後存到workInProgress.memoizedState
中。
在 workInProgress 節點自身處理完成以後,會經過props.children
或者instance.render方法
獲取子 ReactElement。子 ReactElement 多是對象
、數組
、字符串
、迭代器
,針對不一樣的類型進行處理。
數組類型 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 最終形態是這樣子的,但實際上算法是優先深度遍歷,到葉子節點以後再遍歷緊鄰的兄弟節點。若是兄弟節點有子節點,則會繼續擴展下去。
進入這個流程,代表 workInProgress 節點是一個葉子節點,或者它的子節點都已經處理完成了。如今開始要完成這個節點處理的剩餘工做。
completeWork
方法中,會根據workInProgress.tag
來區分出不一樣的動做,下面挑選2個比較重要的來進一步分析:
此前提到過,FiberNode.stateNode
能夠用於存放 DomElement Instance。在初始化過程當中,stateNode 爲 null,因此會經過document.createTextNode
建立一個 Text DomElement,節點內容就是workInProgress.memoizedProps
。最後,經過__reactInternalInstance$[randomKey]
屬性創建與本身的 FiberNode的聯繫。
在本例子中,處理完上面的 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 全貌:
在前面講解基礎數據結構的時候描述過,每一個 FiberNode 上都有 firstEffect、lastEffect ,指向一個Effect(反作用) FiberNode
鏈表。在處理完當前節點,即將返回父節點的時候,把當前的鏈條掛接到 returnFiber 上。最終,在(HostRoot)FiberNode.firstEffect
上掛載着一條擁有當前 FiberNode Tree 全部反作用的 FiberNode 鏈表。
經歷完以前的全部流程,最終 (HostRoot)FiberNode 也被處理完成,就把 (HostRoot)FiberNode 返回,最終做爲finishedWork
返回到 performWorkOnRoot
,後續進入下一個階段。
所謂提交階段,就是實際執行一些周期函數、Dom 操做的階段。
這裏也是一個鏈表的遍歷,而遍歷的就是以前階段生成的 effect 鏈表。在遍歷以前,因爲初始化的時候,因爲 (HostRoot)FiberNode.effectTag
爲Callback
(初始化回調)),會先將 finishedWork 放到鏈表尾部。結構以下:
每一個部分提交完成以後,都會把遍歷節點重置到finishedWork.firstEffect
。
當前這個流程處理的只有屬於 ReactComponent 的 getSnapshotBeforeUpdate
方法。
遍歷到某個節點後,會根據節點的 effectTag 決定進行什麼操做,操做包括插入( Placement )
、修改( Update )
、刪除( Deletion )
。
因爲當前是首次渲染,因此會進入插入( Placement )流程,其他流程將在後面的《How React Works(三)更新流程》中講解。
要作插入操做,必先找到兩個要素:父親 DomElement ,子 DomElement。
經過FiberNode.return
不斷往上找,找到最近的(HostComponent)FiberNode、(HostRoot)FiberNode、(HostPortal)FiberNode節點,而後經過(HostComponent)FiberNode.stateNode
、(HostRoot)FiberNode.stateNode.containerInfo
、(HostPortal)FiberNode.stateNode.containerInfo
就能夠獲取到對應的 DomElement 實例。
遊離子 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 到根節點,則退出。
雖然是短短的一行代碼,但這個十分重要,因此單獨標記:
root.current = finishedWork;
這意味着,在 DomElement 反作用處理完畢以後,意味着以前講的緩衝樹
已經完成任務,翻身當主人,成爲下次修改過程的current
。再來看一個全貌:
在這個流程中,也是遍歷 effect 鏈表,對於每種類型的節點,會作不一樣的處理。
若是當前節點的 effectTag 有 Update 的標誌位,則須要執行對應實例的生命週期方法。在初始化階段,因爲當前的 Component 是第一次渲染,因此應該執行componentDidMount
,其餘狀況下應該執行componentDidUpdate
。
以前講到,updateQueue 裏面也有 effect 鏈表。裏面存放的就是以前各個 Update 的 callback,一般就來源於setState
的第二個參數,或者是ReactDom.render
的 callback
。在執行完上面的生命週期函數後,就開始遍歷這個 effect 鏈表,把 callback 都執行一次。
操做和 ClassComponent 處理的第二部分一致。
這部分主要是處理初次加載的 HostComponent 的獲取焦點問題,若是組件有autoFocus
這個 props ,就會獲取焦點。
本文主要講述了ReactDom.render
的內部的工做流程,描述了 React 初次渲染的內在流程:
child
、return
鏈接sibling
鏈接FiberNode.stateNode
建立關聯。getDerivedStateFromProps
、componentWillMount
方法反作用
不斷往上傳遞,在提交階段裏面,會找到這種標記,並把剛建立完的 DomElement Tree 裝載到容器 DomElement中雙緩衝
的兩棵樹 FiberNode Tree 角色互換,原來的 workInProgress 轉正componentDidMount
下一篇文章將會描述 React 的事件機制(但聽說準備要重構),但願我不會斷耕。
寫完第一篇,React 版本已經到了 16.5.0 ……