- 原文連接:indepth.dev/posts/1009/…
- 原文標題:In-depth explanation of state and props update in React
- 原文做者:Max Koretskyi
在個人上篇文章 Inside Fiber: 深刻了解React新協調算法中介紹了理解更新過程細節的所需的基礎知識,我將在本文中描述這個更新過程。html
我已經概述了將在本文中使用的主要數據結構和概念,特別是Fiber節點,current
和work-in-progress
樹,反作用(side-effects)以及effects鏈表(effects list)。我也提供了主要算法的高級概述和render
階段與commit
階段的差別。若是你尚未閱讀過它,我推薦你從那兒開始。react
我還向你介紹了帶有一個按鈕的示例程序,這個按鈕的功能就是簡單的增長數字。git
你能夠在這查看在線代碼。它的實現很簡單,就是一個render函數中返回button
和span
元素的類組件。當你點擊按鈕的時候,在點擊事件的處理函數中更新組件的state
。結果就是span
元素的文本會更新。github
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
componentDidUpdate() {}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>
]
}
}
複製代碼
我爲這個組件添加了componentDidUpdate
生命週期方法。這是爲了演示React如何添加effects並在commit
階段調用這個方法。在本文中,我想向你展現React是如何處理狀態更新和建立effects list的。咱們能夠看到render
階段和commit
階段的高級函數中發生了什麼。算法
尤爲是在React的completeWork函數中:數組
ClickCounter
的state
中的count
屬性render
方法獲取子元素列表並比較span
元素的props
以及,在React的commitRoot 函數中:瀏覽器
span
元素的文本內容屬性componentDidUpdate
生命週期方法可是在那以前,咱們先快速看看當咱們在點擊處理函數中調用setState
時工做是如何調度的。安全
請注意,你無需瞭解這些來使用React。本文是關於React內部是如何運做的。markdown
當咱們點擊按鈕時,click
事件被觸發,React執行傳遞給按鈕props
的回調。在咱們的程序中,它只是簡單的增長計數器和更新state
。數據結構
class ClickCounter extends React.Component {
...
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
}
複製代碼
每一個組件都有相應的updater
,它做爲組件和React核心之間的橋樑。這容許setState
在ReactDOM,React Native,服務端渲染和測試程序中是不一樣的實現。(譯註:從源碼能夠看出,setState
內部是調用updater.enqueueSetState
,這樣在不一樣平臺,咱們均可以調用setState
來更新頁面)
本文中,咱們關注ReactDOM中實現的updater
對象,它使用Fiber協調器。對於ClickCounter
組件,它是classComponentUpdater。它負責獲取Fiber實例,爲更新入列,以及調度work。
當更新排隊時,它們基本上只是添加到Fiber節點的更新隊列中進行處理。在咱們的例子中,ClickCounter
組件對應的Fiber節點將有下面的結構:
{
stateNode: new ClickCounter,
type: ClickCounter,
updateQueue: {
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) => { return {count: state.count + 1} }
}
},
...
},
...
}
複製代碼
如你所見,updateQueue.firstUpdate.next.payload
中的函數就我咱們在ClickCounter
組件中傳遞給setState
的回調。它表明在render
階段中須要處理的第一個更新。
我上篇文章中的work循環部分中解釋了全局變量nextUnitOfWork
的角色。尤爲是,這個變量保存workInProgress
樹中有work待作的Fiber節點的引用。當React遍歷樹的Fiber時,它使用這個變量知道是否存在其餘有未完成work的Fiber節點。
咱們假定setState
方法已經被調用。 React將setState中的回調添加到ClickCounter
fiber節點的updateQueue
中,而後調度work。React進入render
階段。它使用renderRoot函數從最頂層HostRoot
Fiber節點開始遍歷。然而,它會跳過已經處理過得Fiber節點直到遇到有未完成work的節點。基於這點,只有一個節點有work待作。它就是ClickCounter
Fiber節點。
全部的work都是基於保存在Fiber節點的alternate
字段的克隆副本執行的。若是alternate節點還未建立,React在處理更新前調用createWorkInProgress函數建立副本。咱們假設nextUnitOfWork
變量保存代替ClickCounter
Fiber節點的引用。
首先, 咱們的Fiber進入beginWork函數。
由於這個函數對樹中每一個節點執行,因此若是你想調試render階段,它是放置斷點的好地方。 我常常這樣作,還有檢查Fiber節點的type來肯定我須要的節點。
beginWork
函數大致上是個大的switch
語句,經過tag
肯定Fiber節點須要完成的work的類型,而後執行相應的函數來執行work。在這個例子中,CountClicks
是類組件,因此會走這個分支:
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
...
case FunctionalComponent: {...}
case ClassComponent:
{
...
return updateClassComponent(current$$1, workInProgress, ...);
}
case HostComponent: {...}
case ...
}
複製代碼
咱們進入updateClassComponent函數。取決於它是首次渲染、恢復work仍是React更新,React會建立實例並掛載組件或只是更新它:
function updateClassComponent(current, workInProgress, Component, ...) {
...
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
...
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, ...);
mountClassInstance(workInProgress, Component, ...);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
} else {
shouldUpdate = updateClassInstance(current, workInProgress, ...);
}
return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}
複製代碼
咱們已經有了ClickCounter
組件實例,因此咱們進入updateClassInstance。這是React爲類組件執行大部分work的地方。如下是在這個函數中按順序執行的最重要的操做:
UNSAFE_componentWillReceiveProps()
鉤子(已廢棄)updateQueue
中的更新以及生成新stategetDerivedStateFromProps
並獲得結果shouldComponentUpdate
肯定組件是否須要更新;若是返回結果爲false
,跳過整個渲染過程,包括在該組件和它的子組件上調用render
;不然繼續更新UNSAFE_componentWillUpdate
(已廢棄)componentDidUpdate
生命週期鉤子儘管調用
componentDidUpdate
的effect是在render
階段添加的,這個方法將在接下來的commit
階段執行。
state
和props
組件實例的state
和props
應該在render
方法調用前更新,由於render
方法的輸出一般依賴於state
和props
。若是咱們不這樣作,它每次都會返回同樣的輸出。
下面是該函數的簡化版本:
function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps;
instance.props = oldProps;
if (oldProps !== newProps) {
callComponentWillReceiveProps(workInProgress, instance, newProps, ...);
}
let updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
processUpdateQueue(workInProgress, updateQueue, ...);
newState = workInProgress.memoizedState;
}
applyDerivedStateFromProps(workInProgress, ...);
newState = workInProgress.memoizedState;
const shouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...);
if (shouldUpdate) {
instance.componentWillUpdate(newProps, newState, nextContext);
workInProgress.effectTag |= Update;
workInProgress.effectTag |= Snapshot;
}
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
複製代碼
上面代碼片斷中我刪除了一些輔助代碼。對於實例,調用生命週期方法或添加effects來觸發它們前,React使用typeof
操做符檢查組件是否實現了這些方法。好比,這是React添加effect前如何檢查componentDidUpdate
方法:
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
複製代碼
好的,咱們如今知道了render
階段中爲ClickCounter
Fiber節點執行了什麼操做。如今讓咱們看看這些操做如何改變Fiber節點的值。當React開始work,ClickCounter
組件的Fiber節點相似這樣:
{
effectTag: 0,
elementType: class ClickCounter, firstEffect: null, memoizedState: {count: 0},
type: class ClickCounter, stateNode: {
state: {count: 0}
},
updateQueue: {
baseState: {count: 0},
firstUpdate: {
next: {
payload: (state, props) => {…}
}
},
...
}
}
複製代碼
work完成後,咱們獲得一個長這樣的Fiber節點:
{
effectTag: 4,
elementType: class ClickCounter, firstEffect: null, memoizedState: {count: 1},
type: class ClickCounter, stateNode: {
state: {count: 1}
},
updateQueue: {
baseState: {count: 1},
firstUpdate: null,
...
}
}
複製代碼
花點時間觀察屬性值的差別
更新被應用後,memoizedState
和updateQueue
中baseState
的屬性count
的值變爲1
。React也更新了ClickCounter
組件實例的state。
至此,隊列中再也不有更新,因此firstUpdate
爲null
。更重要的是,咱們改變了effectTag
屬性。它再也不是0
,它的是爲4
。 二進制爲100
,意味着第三位被設置了,表明Update
反作用標記:
export const Update = 0b00000000100;
複製代碼
能夠得出結論,當執行ClickCounter
Fiber節點的work時,React低啊用變化前生命週期方法,更新state,定義有關的反作用。
在那以後,React進入finishClassComponent。這是調用組件實例render方法和在子組件上使用diff算法的地方。文檔中對此有高級概述。如下是相關部分:
當比較兩個相同類型的React DOM元素時,React查看二者的屬性(attributes),保留DOM節點,僅更新變化了的屬性。
然而,若是咱們深刻挖掘,會知道它實際是對比Fiber節點和React元素。可是我如今不會詳細介紹由於過程至關複雜。我會單獨些篇文章,特別關注子協調過程。
若是你想本身學習細節,請查看reconcileChildrenArray函數,由於在咱們的程序中
render
方法返回一個React元素數組。
至此,有兩個很重要的事須要理解。第一,當React進行子協調時,它會爲從render
函數返回的子React元素建立或更新Fiber節點。finishClassComponent
函數當前Fiber節點的第一個子節點的引用。它被賦值給nextUnitOfWork
並在稍後的work循環中處理。第二,React更新子節點的props做爲父節點執行的一部分work。爲此,它使用render函數返回的React元素的數據。
舉例來講,這是React協調ClickCounter
fiber子節點以前span
元素對應的Fiber節點看起來的樣式
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 0},
...
}
複製代碼
能夠看到,memoizedProps
和pendingProps
的children
屬性都是0
。這是render
函數返回的span
元素對應的React元素的結構。
{
$$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
複製代碼
能夠看出,Finer節點和返回的React元素的props是有差別的。createWorkInProgress內部用這建立替代的Fiber節點,React把React元素中更新的屬性複製到Fiber節點。
所以,在React完成ClickCounter
組件子協調後,span
的Fiber節點的pendingProps
更新了。它們將匹配span
React元素中的值。
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
複製代碼
稍後,React會爲span
Fiber節點執行work,它將把它們複製到memoizedProps
以及添加effects來更新DOM。
好的,這就是render階段React爲ClickCounter
fiber節點所執行的全部work。由於button是ClickCounter
組件的第一個子節點,它會被賦值給nextUnitOfWork
變量。button上無事可作,全部React會移動到它的兄弟節點span
Fiber節點上。根據這裏描述的算法,這發生在completeUnitOfWork
函數內。
nextUnitOfWork
變量如今指向span
fiber的alternate,React基於它開始工做。和ClickCounter
執行的步驟相似,開始於beginWork函數。
由於span
節點是HostComponent
類型,此次在switch語句中React會進入這條分支:
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionalComponent: {...}
case ClassComponent: {...}
case HostComponent:
return updateHostComponent(current, workInProgress, ...);
case ...
}
複製代碼
結束於updateHostComponent函數。(在這個函數內)你能夠看到一系列和類組件調用的updateClassComponent
函數相似的函數。對於函數組件是updateFunctionComponent
。你能夠在ReactFiberBeginWork.js文件中找到這些函數。
在咱們的例子中,span
節點在updateHostComponent
裏沒什麼重要事的發生。
一旦beginWork
完成,節點就進入completeWork
函數。可是在那以前,React須要更新span
Fiber節點的memoizedProps
屬性。你應該還記得協調ClickCounter
組件子節點時更新了span
Fiber節點的pendingProps
。
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
複製代碼
因此一旦span
fiber的beginWork
完成,React會將pendingProps
更新到memoizedProps
。
function performUnitOfWork(workInProgress) {
...
next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
...
}
複製代碼
而後調用的completeWork
和咱們看過的beginWork
類似,基本上是一個大的switch語句。
function completeWork(current, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionComponent: {...}
case ClassComponent: {...}
case HostComponent: {
...
updateHostComponent(current, workInProgress, ...);
}
case ...
}
}
複製代碼
因爲span
Fiber節點是HostComponent
,它會執行updateHostComponent函數。在這個函數中React大致上作了這些事:
span
fiber的updateQueue
在這些操做執行前,span
Fiber節點看起來像這樣:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 0
updateQueue: null
...
}
複製代碼
works完成後它看起來像這樣:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 4,
updateQueue: ["children", "1"],
...
}
複製代碼
注意effectTag
和updateQueue
字段的差別。它再也不是0
,它的值是4
。用二進制表示是100
,意味着設置了第3位,正是Update
反作用的標誌位。這是React在接下來的commit階段對這個節點惟一要作的任務。updateQueue
保存着用於更新的載荷。
一旦React處理完ClickCounter
級它的子節點,render
階段結束。如今它能夠將完成的替代樹賦值給FiberRoot
的finishedWork
屬性。這是須要被刷新到屏幕上的新樹。它能夠在render
階段以後立刻被處理,或這當React被瀏覽器給予時間時再處理。
在咱們的例子中,因爲span
節點ClickCounter
組件有反作用,React將添加指向span
Fiber節點的連接到HostFiber
的firstEffect
屬性。
React在compliteUnitOfWork函數內構建effects list。這是帶有更新span
節點文本和調用ClickCounter
上hooks反作用的Fiber樹看起來的樣子:
這是由有反作用的節點組成的線性列表:
這個階段開始於completeRoot函數。它在作其餘工做以前,它將FiberRoot
的finishedWork
屬性設爲null
:
root.finishedWork = null;
複製代碼
於以前的render
階段不一樣的是,commit
階段老是同步的,這樣它能夠安全地更新HostRoot
來表示commit work開始了。
commit
階段是React更新DOM和調用突變後生命週期方法componentDidUpdate
的地方。爲此,它遍歷在render
階段中構建的effects list並應用它們。
有如下在render
階段爲span
和ClickCounter
定義的effects:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
複製代碼
ClickCounter
的effect tag的值是5
或二進制的101
,定義了對於類組件基本上轉換爲componentDidUpdate
生命週期方法的Update
工做。最低位也被設置了,表示該Fiber節點在render
階段的全部工做都已完成。
span
的effect tag的值是4
或二進制的100
,定義了原生組件DOM更新的update
工做。這個例子中的span
元素,React須要更新這個元素的textContent
。
讓咱們看看React如何應用這些effects。commitRoot函數用於應用這些effects,由3個子函數組成:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
複製代碼
每一個子函數都實現了一個循環,該循環用於遍歷effects list並檢查這些effects的類型。當發現effect和函數的目的有關時就應用它。咱們的例子中,它會調用ClickCounter
組件的componentDidUpdate
生命週期方法,更新span
元素的文本。
第一個函數 commitBeforeMutationLifeCycles 尋找 Snapshot effect而後調用getSnapshotBeforeUpdate
方法。可是,咱們在ClickCounter
組件中沒有實現該方法,React在render
階段沒有添加這個effect。因此在咱們的例子中,這個函數不作任何事。
接下來React執行 commitAllHostEffects 函數。這兒是React將span
元素的t文本由0
變爲1
的地方。ClickCounter
fiber沒有要作的,由於類組件的節點沒有任何DOM更新。
這個函數的主旨是選擇正確類型的effect並應用相應的操做。在咱們的例子中咱們須要跟新span
元素的文本,因此咱們採用Update
分支:
function updateHostEffects() {
switch (primaryEffectTag) {
case Placement: {...}
case PlacementAndUpdate: {...}
case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {...}
}
}
複製代碼
隨着commitWork
執行,最終會進入updateDOMProperties函數。它使用在render
階段添加到Fiber節點的updateQueue
載荷更新span
元素的textContent
。
function updateDOMProperties(domElement, updatePayload, ...) {
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) { ...}
else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...}
else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {...}
}
}
複製代碼
應用DOM更新後,React將finishedWork
賦值給HostRoot
。它將替代樹是設爲當前樹:
root.current = finishedWork;
複製代碼
剩下的函數是commitAllLifecycles。這是 React 調用突變後生命週期方法的地方。在render
階段,React爲ClickCounter
組件添加Update
effect。這是commitAllLifecycles
尋找的effects之一併調用componentDidUpdate
方法:
function commitAllLifeCycles(finishedRoot, ...) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLifeCycles(finishedRoot, current, nextEffect, ...);
}
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
複製代碼
這個函數也更新refs,可是因爲咱們沒有使用這個特性,因此沒什麼做用。這個方法在commitLifeCycles函數中被調用:
function commitLifeCycles(finishedRoot, current, ...) {
...
switch (finishedWork.tag) {
case FunctionComponent: {...}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
...
instance.componentDidUpdate(prevProps, prevState, ...);
}
}
}
case HostComponent: {...}
case ...
}
複製代碼
也能夠看出,這是首次渲染時React調用組件componentDidMount
方法的函數。