本文爲意譯和整理,若有誤導,請放棄閱讀。原文。html
這篇文章用一個由parent component和children component組成的例子來說述fiber架構中react將props傳遞給子組件的處理流程。node
在我先前的文章中 Inside Fiber: in-depth overview of the new reconciliation algorithm in React 提到要想理解更新流程的技術細節,咱們需得具有必定的基礎知識。而這部分的基礎知識就是篇文章要講述的內容。react
對於本文所提到的數據結構和概念,我已經在上一篇文章概述過了。這些數據結構和概念主要包括有:git
同時,我也對主要算法進行了宏觀上的闡述,也解釋過render階段和commit階段之間的差別性。若是你尚未閱讀過講述這些東西的文章,我建議你先去閱讀。github
我也引入過一個簡單demo。這個demo的主要功能是經過點擊button來增長界面上的一個數字。 算法
你能夠這裏去玩玩它。這個demo實現了一個簡單的組件。這個組件的render方法返回了兩個子組件:button和span。當你點擊界面上的按鈕的時候,咱們會在click的事件處理器中去更新組件的state。結果是,界面上span元素的文本內容獲得更新。數組
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是如何處理state更新和構建effects list的。咱們也對render
和commit
階段的頂層函數進行簡單的講解。安全
特別地,咱們着重看看completeWork
方法:bash
ClickCounter
組件state中的count
屬性。render
方法,獲取到children列表,而後執行比對。和commitRoot
方法:
textContent
屬性。componentDidUpdate
這個生命週期函數。在深刻這些東西以前,咱們快速地過一遍「當咱們在click事件處理器中調用setState的時候,work是如何被調度」的這一環節。
當咱們點擊界面上的button的時候,click事件被觸發了,而後React執行咱們做爲props傳遞進去的事件回調。在咱們的demo中,這個事件回調就是簡單地經過增長count字段值來更新組件的狀態。
class ClickCounter extends React.Component {
...
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
}
複製代碼
每個React組件都有本身的updater
,這個updater
充當着組件與React core通信的橋樑。這種設計,使得多個render(好比:ReactDOM, React Native, server side rendering和testing utilities)去實現本身的setState方法成了可能。
在這篇文章中,咱們單獨分析一下updater對象在ReactDOM中的實現。在這個實現中,就用到了Fiber reconciler。具體對於ClickCounter
組件來講,這個updater對象就是classComponentUpdater。它的職責有:1)把Fiber的實例檢索回來; 2)將更新請求入隊;3)對work進行調度。
當咱們說「一個更新請求被入隊」,其實意思就是把一個setState的callback添加到Fiber node的「updateQueue」隊列中去,等待處理。迴歸到本示例,ClickCounter
組件所對應的Fiber node具體的數據結構:
{
stateNode: new ClickCounter,
type: ClickCounter,
updateQueue: {
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) => { return {count: state.count + 1} }
}
},
...
},
...
}
複製代碼
正如你所看到的那樣,updateQueue.firstUpdate.next.payload
引用所指向的那個函數就是咱們傳給setState方法的那個callback。它表明着render
階段第一個須要被處理的「更新請求」。
在我先前的那篇文章關於work loop的那一章節中,我已經解釋過nextUnitOfWork
這個全局變量所扮演的角色了。特別地,這一章節說到了這個全局變量指向的是workInProgress
tree上那些有work須要去作的Fiber node。當React遍歷整顆Fiber樹的時候,就是用這個全局變量來判斷是否還有未完成本身的work的Fiber node。
咱們從setState方法已經被調用的地方開始提及。在setState方法被調用以後,React會把咱們傳給setState的callback傳遞ClickCounter
fiber node,也就是說把這個callback添加到fiber node的updateQueue
對象中。而後,就開始調度work。也是從這裏開始,React開始進入了render
階段了。它調用renderRoot這個函數,從最頂層的HostRoot
開始遍歷整顆fiber node樹。儘管是從最頂層的根節點開始,可是React會掉過那些已經處理過的 fiber node,只會處理那些還有work須要去完成的節點。此時此刻,咱們只有一個fiber node是有work須要去作的。這個node就是ClickCounter
fiber node。
ClickCounter
fiber node的alternate
字段用於保存一個指向[當前fiber node的克隆副本]的引用。這個克隆副本上的work都是已經執行完成的了。這個克隆副本被稱爲當前fiber node的alternate fiber node
。若是alternate fiber node
尚未被建立的話,那麼React就會在處理更新請求以前使用createWorkInProgress函數去完成複製工做。如今,咱們假設變量nextUnitOfWork
保存着指向當前fiber node的alternate fiber node
的引用。
首先,咱們的fiber node將會被傳遞到beginWork 函數裏面。
由於這個函數會在fiber node tree上的每個節點調用。因此,若是你想調試
render
階段,這是一個打斷點的好地方。我常常這麼幹,經過檢測fiber node的type值來肯定當前節點是不是我要跟進的那個。
beginWork
函數基本上就是一個大的switch語句。在這個switch語句,beginWork
根據workInProgress的tag值來計算初當前fiber node所須要完成的work的類型。而後,執行相應的函數去執行這個work。在咱們的demo中,由於ClickCounter
是一個class component,因此,咱們會執行如下的分支語句:
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
...
case FunctionalComponent: {...}
case ClassComponent:
{
...
return updateClassComponent(current$$1, workInProgress, ...);
}
case HostComponent: {...}
case ...
}
複製代碼
那麼,咱們會進入updateClassComponent函數中。取決於當前:1)是不是組件的首次渲染:2)是不是work正在被恢復執行;3)是不是一次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 will 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執行了class component絕大部分的work。如下是這個方法執行的最重要的操做(羅列的順序也是代碼執行的順序):
UNSAFE_componentWillReceiveProps
生命週期函數(已棄用);updateQueue
中的更新請求和生成一個新的state值;getDerivedStateFromProps
,並獲取調用結果。shouldComponentUpdate
來確保一個組件是否真的想要更新。若是調用返回值爲false的話,那麼React將會跳過整個渲染流程包括調用組件實例和它的子組件實例的render方法。不然的話,正常走更新流程。UNSAFE_componentWillUpdate
生命週期函數(已棄用);componentDidUpdate
添加成一個effect。雖然,「調用
componentDidUpdate
」這個effect是在render
階段添加的,可是這個方法的實際執行是在接下來的commit
階段。
state和props值的更新應該是在render方法調用前的。由於render的返回值是須要依賴最新的state和props值(譯者注:這也是指出了一個事實,即react組件更新的本質就是用最新的state和props值去調用組件實例的render方法)。若是咱們不這麼幹的話,那麼render方法的每一次調用的返回值都是同樣的。
下面是updateClassInstance
方法的精簡版:
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;
}
複製代碼
我已經把一些比較次要的代碼移除掉了。舉個例子,在調用生命週期函數和添加effect並觸發它以前,React會用typeof操做符去檢查這個組件是否實現了某個方法。下面的代碼中,React會在添加effect以前檢查componentDidUpdate
方法是不是一個function:
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
複製代碼
到了這裏,咱們已經知道在render
階段,ClickCounter
fiber node須要執行哪些操做了。下面,咱們來看看,這些操做是如何改變fiber node上的相關值的。當React開始執行work的時候,ClickCounter
組件所對應的fiber node長這樣的:
{
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執行完畢,ClickCounter
組件所對應的fiber node已經長成這樣的:
{
effectTag: 4,
elementType: class ClickCounter,
firstEffect: null,
memoizedState: {count: 1},
type: class ClickCounter,
stateNode: {
state: {count: 1}
},
updateQueue: {
baseState: {count: 1},
firstUpdate: null,
...
}
}
複製代碼
仔細觀察一下連個fiber node屬性值之間的差別。咱們會發現,在處理完更新請求後,memoizedState和baseState中的count字段的屬性值已經變爲1了。與此同時,React也把ClickCounter
的組件實例的狀態也更新了。
當前,咱們在updateQueue中已經沒有更新請求了,因此firstUpdate
的值爲null。還有很重要的一點,咱們的effectTag
字段的值已經從0變爲4了。4用二進制表示就是100
,而這就是update
這個side-effect的tag值:
export const Update = 0b00000000100;
複製代碼
下面作個小總結。當React在ClickCounter
fiber node上執行work的時候,React要作的事有:
當上面提到的小總結的東西完成後,React執行將會進入finishClassComponent
。在這個函數裏面,React將會調用組件實例的render
方法,而後在它的子組件實例(正是render方法返回的東西)上應用diff算法。在這篇文章裏面有一個關於diff算法高質量的歸納:
當對比中的兩個react DOM element(譯者注:本質上就是react element,可是type的值是DOM類型的字符串)具體相同的type的時候,React會查看二者的attribute的差別性,保留底層所對應的DOM node對象,只是更新那些須要改變的attribute。
若是咱們再深究一點的話,那麼,咱們會了解到其實對比是react element所對應的fiber node。在本文中,我不會討論太多細節,由於這裏面的處理流程仍是挺複雜的。我將會在一個單獨的文章上專門來說述child reconciliation的處理流程。
若是你着急去了解child reconciliation細節的話,那麼你能夠查看這個reconcileChildrenArray函數。由於在咱們這個demo中,
ClickCounter
的render方法返回的是一個react element組成的數組。
當前,有兩件重要的事情須要咱們去理解。第一件是,隨着child reconciliation流程的執行,React會爲從render方法中返回的child react element建立或者更新對應的fiber node。finishClassComponent函數會返回當前fiber node第一個child fiber node的引用。這個引用將會賦值給nextUnitOfWork
,而且會在work loop的下一個循環中使用到;第二件事是,React把對子fiber node 的props的更新看成父fiber node的work的一部分。爲了達成這事,React會使用從render方法返回的react element身上的數據。
舉個例子,在React對ClickCounter
fiber node 的children進行reconcile以前,span元素所對應的fiber node是長這樣的:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 0},
...
}
複製代碼
正如你所見的那樣,memoizedProps
和pendingProps
中的children屬性值都是0 。而下面,就是調用render方法後返回的span元素所對應的react element:
{
$$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
複製代碼
正如你所見的那樣,fiber node中的props與返回的react element中的props是不一樣的。在createWorkInProgress函數中,這種不一樣性會應用 在alternate fiber node的建立過程當中。React就是從react element上拷貝已經更新的props到alternate fiber node上的。
當React對ClickCounter
組件的children完成了reconcile以後,span元素所對應的fiber node的pendingProps字段的值將獲得更新。該字段值將會跟span元素所對應的react element的props值保持一致:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
複製代碼
稍後,React會span元素所對應的fiber node執行work,它會將它們複製到memoizedProps
上,並向DOM更新上添加effect(add effect to DOM update)。
到此爲止,咱們已經講完了ClickCounter
fiber node在render階段所須要執行的全部的work了。由於button組件是ClickCounter
組件的第一個子元素,因此,它所對應的fiber node將會被賦值給nextUnitOfWork
變量。由於這個fiber node沒有任何work須要去作的。因此,React會移步到它的sibling-span元素所對應的fiber node。根據這裏所描述的算法能夠得知,以上過程發生在completeUnitOfWork
函數裏面。
因此,nextUnitOfWork
變量如今指向span元素所對應的fiber node(後面簡稱爲「span fiber node」)的alternate fiber node。React對span fiber node的更新處理流程就是從這裏開始。跟ClickCounter
fiber node的處理流程是同樣的,咱們都是從beginWork函數開始。
由於span節點屬於HostComponent
類型的,因此,這一次,咱們會進入HostComponent的分支:
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionalComponent: {...}
case ClassComponent: {...}
case HostComponent:
return updateHostComponent(current, workInProgress, ...);
case ...
}
複製代碼
最終,咱們會進入updateHostComponent這個函數。往上,你能夠看到咱們上面在分析ClickCounter
fiber node時候的所提到的updateClassComponent
,針對functional component,React會執行updateFunctionComponent等等。你能夠在ReactFiberBeginWork.js文件中找到全部的這些函數的實現代碼。
在咱們的demo中,由於span節點的子節點太過簡單了,因此在updateHostComponent函數中,沒啥過重要的事情發生。
一旦beginWork執行完畢,當前fiber node就會被傳遞到completeWork中去。在本示例中,這個fiber node就是span fiber node。在此以前,React須要更新span fiber node上的memoizedProps
字段值。你可能還記得,當React對ClickCounter
組件的子組件進行reconcile的時候,它已經更新span fiber node上的pendingProps字段:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
複製代碼
因此,一旦beginWork函數在span fiber node上調用完畢的話,那麼React會更新memoizedProps字段值,使得它與pendingProps字段值保持一致:
function performUnitOfWork(workInProgress) {
...
next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
...
}
複製代碼
執行完beginWork
函數後,React就會執行completeWork
函數。這個函數的實現基本上就是一個大大的switch語句。這跟以前所提到的beginWork裏面的switch語句差很少:
function completeWork(current, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionComponent: {...}
case ClassComponent: {...}
case HostComponent: {
...
updateHostComponent(current, workInProgress, ...);
}
case ...
}
}
複製代碼
由於咱們的span fiber node(所對應的react element)是HostComponent
,因此,咱們會進入到updateHostComponent函數裏面。在這個函數裏面,React基本上就作了如下的三件事情:
在執行這行操做以前,span fiber node長這樣的:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 0
updateQueue: null
...
}
複製代碼
當上面的work執行完成後,span fiber node長這樣:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 4,
updateQueue: ["children", "1"],
...
}
複製代碼
請注意二者在effectTag和updateQueue字段值上的不一樣。對於effectTag的值來講,它再也不是0,而是4。用二進制表示就是100
,而第三位就是update這 種side-effect所對應的二進制位。現在該位置爲1,則說明span fiber node後面所須要執行的side-effect就是update
。在接下來的commit階段,對於span fiber node來講,這也是React惟一須要幫它完成的任務了。而updateQueue字段值保存的是用於update的數據。
一旦React處理完ClickCounter
fiber node和它的子fiber node們,那麼render階段算是結束了。React會把產出的alternate fiber node樹賦值給FiberRoot對象的finishedWork屬性。這顆新的alternate fiber node樹包含了須要被flush到屏幕的東西。它會在render階段以後立刻被處理或者稍後在瀏覽器分配給React的,空閒的時間裏面執行。
在咱們給出的示例中,由於span fiber node和ClickCounter fiber node是有side effect的。React將會給span fiber node添加一個link,讓它指向HostFiber的firstEffect屬性
在函數compliteUnitWork中,react完成了effect list的構建。下面就是本示例中,帶有effect的fiber node樹。在這棵樹上,有着兩個effect:1)更新span節點的文本內容;2)調用ClickCounter
組件的生命週期函數:
而下面是由具備effect的fiber node組成的線性列表:
這個階段以completeRoot函數開始。在繼續往下走以前,它首先將FiberRoot的finishedWork屬性值置爲null:
root.finishedWork = null;
複製代碼
不像render階段,commit階段是同步執行的。因此,它能很安全地更新HostRoot,以此來指示commit工做已經開始了。
commit階段是React進行DOM操做和調用post-mutation生命週期方法componentDidUpate的地方。爲了實現上面這些目標,React會遍歷render階段所產出的effect list,並應用相應的effect。
就本示例而言,咱們在render階段事後,咱們有如下幾個effect:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
複製代碼
ClickCounter
fiber node的effect tag爲5,用二進制表示就是「101」。它對應的work是update
。而對於class component而言,這個work會被「翻譯爲」componentDidUpdate這個生命週期方法。在二進制「101」中,最低位爲「1」,表明着當前這個fiber node的全部work都在render階段執行完畢了。
span fiber node的effect tag值是4,用二進制表示是「100」。這個編號所表明的work是「update」,由於當前的span fiber node對應的是host component類型的。這個「update」work更具體點來講就是「DOM更新」。迴歸到本示例,「DOM更新」更具體點是指「更新span元素的textContent屬性」。
讓咱們一塊兒來看看,React是如何應用這些effect的。函數commitRoot就是用來應用effect的。它由三個子函數組成:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
複製代碼
這三個子函數都實現對effect list的遍歷,而且在遍歷過程當中去檢查effect的類型。若是它們發現當前的這個effect跟它們函數的職責相關的,那麼就會應用這個effect。在咱們的示例中,具體點講就是在ClickCounter
組件上調用componentDidUpdate
這個生命週期方法和更新span元素的文本內容。
第一個子函數commitBeforeMutationLifeCycles 會查找snapshot
類型的effect,並調用getSnapshotBeforeUpdate方法。由於在ClickCouner
組件身上,咱們並無實現這個方法,因此React並無在render階段把這個effect添加到該組件對應的fiber node身上。因此,在咱們這個示例中,這個子函數啥事都沒作。
接下來,React會移步到commitAllHostEffects函數上面來。就是在這個函數裏面,React完成了將span元素的文本內容從「0」更新到「1」。這個函數幾乎跟ClickCounter
這個fiber node沒有關係。由於這個fiber node對應的是class component,而class componnet是沒有任何的直接的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 node的updateQueue字段身上的payload來更新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更新]這個effect被應用後,React將finishedWork樹賦值給HostRoot。它把alternate tree設置爲current tree:
root.current = finishedWork;
複製代碼
咱們剩下最後一個commitAllLifecycles要講了。在這個函數裏面,React調用了全部的post-mutational 生命週期方法。在render階段,React往ClickCounter
組件身上添加了一個叫「update」的effect。這個effect就是本函數所要查找的effect,一旦找到以後,React就會調用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,可是由於咱們這個示例中並無使用到這個特性。因此相應的那部分代碼(指commitAttachRef(nextEffect);
)就不會被執行。對componentDidUpdate方法的調用是發生在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這個生命週期方法的地方。不過這個調用時機是在組件的首次掛載的過程當中而已。