原文連接:medium.com/react-in-de…javascript
到react fiber內部一探究竟html
在我以前文章Fiber內部:深度概述React新協調算法中,我鋪設了基礎內容,用於理解我這篇文章講解的更新處理的技術細節。java
我已經概述過將在這篇文章中用到的主要數據結構和概念,特別是Fiber節點、當前和工做過程樹、反作用和做用列表,我也對主要的算法提供過大體的說明,且解釋過**render
和commit
**階段的不一樣。若是你尚未讀過,那我建議你從那裏仍是react
我也介紹過一個button的簡單應用,在屏幕上渲染一個遞增的樹:git
你能夠在這裏運行它,它實現了一個簡單的組件,經過**render
方法返回兩個子元素button
和span
。當你點擊button時,組件的狀態就會在處理方法中更新,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如何在commit
**階段添加做用(effects)來調用這個方法。算法
這篇文章中,我想向你展現React如何處理狀態更新以及構建做用列表,咱們將帶你去看看**render
和commit
**階段中大體方法都作了些什麼。數組
特別的是,咱們將看到React在completeWork
中是如何:瀏覽器
ClickCounter
的state
中的count
**屬性。render
**方法來獲取子節點列表,以及執行比較。span
**元素的props還有,React在commitRoot
:安全
span
元素的textCount
**屬性。componentDidUpdate
**生命週期方法。在這以前,咱們先看看,當咱們在click處理方法中調用**setState
**時,工做(就是指的work loop中的work啦)如何如何調用的。
注意,你須要知道這裏的一塊兒來用React,這篇文章是關於React如何內部工做的。
當咱們點擊button時,**click
**事件被觸發,React執行經過props傳給button的回調方法,在咱們的應用中,它簡單的增長計數器,並更新狀態:
class ClickCounter extends React.Component {
...
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
}
複製代碼
每個React組件都有相關聯**updater
,它扮演組件與React內核的橋,這使得setState
**在ReactDOM、React Native、服務端渲染以及測試工具中有不一樣的實現。
這篇文章中,咱們將看看ReactDOM中updater對象的實現,它使用了Fiber協調器。對於**ClickCounter
**組件,它是classComponentUpdater
,它負責獲取Fiber實例、隊列化更新以及調度工做。
當更新隊列化了,它們就在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
**階段須要處理的第一個更新。
個人前一篇文章的工做循環章節解釋了**nextUnitOfWork
全局變量的角色,特別是,它持有了來自還有工做待作的workInProgress
**樹中的Fiber節點。當React遍歷Fiber樹時,使用它來知道是否要有其餘未完成工做的Fiber節點。
咱們從假定**setState
方法被調開始,React在ClickCounter
上添加setState
的回調,且調用工做,React進入render
階段。它使用renderRoot方法從頂層HostRoot
開始遍歷,而後它會調用以及處理過的fiber節點,直到發現尚未完成工做的節點,在這一點上,這裏只有一個fiber節點有工做作,就是ClickCounter
**Fiber節點。
全部工做都在fiber的副本上執行,這個副本保存在**alternate
字段中,若是這個alternate節點尚未建立,React會在處理更新以前在createWorkInProgress方法中建立這個副本。咱們來假定這個nextUnitOfWork
遍歷就持有這個副本ClickCounter
**Fiber節點的引用。
首先,咱們Fiber進入beginWork方法。
由於這個方法在樹中的每一個fiber節點上都會執行,因此若是你想在**
render
**階段debug,那這是很好斷點位置,我常常這樣來檢查Fiber節點類型,以便於肯定我須要的那個節點。
**beginWork
基本上是一個大的switch
語句,根據tag來肯定每一個Fiber須要作的工做類型,而後執行各自的方法,在咱們的CountClicks
**例子中,它是個類組件,因此這部分被執行:
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
...
case FunctionalComponent: {...}
case ClassComponent:
{
...
return updateClassComponent(current$$1, workInProgress, ...);
}
case HostComponent: {...}
case ...
}
複製代碼
咱們進入updateClassComponent
方法,依賴於它是首次渲染、工做恢復繼續(work不是能夠異步打斷的嘛),或者只是更新,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處理類組件大部分工做的地方,方法中按順序有最重要的幾個操做:
UNSAFE_componentWillReceiveProps
鉤子 (棄用)updateQueue
**中的更新,並生成新的stategetDerivedStateFromProps
**,並得到結果shouldComponentUpdate
確保組件是否須要更新,若是不,則跳過整個render處理,包括該組件和其子組件的render
**調用,反之則用更新處理。UNSAFE_componentWillUpdate
** (棄用)componentDidUpdate
**生命週期鉤子儘管調用**
componentDidUpdate
的做用在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;
}
複製代碼
在上面的代碼片斷中我已經移除掉一些輔助代碼,對於實例,在調用生命週期方法或者添加觸發它們的做用前,React使用typeof操做符檢測組件是否實現了這個方法。例如,這裏即是React檢測**componentDidUpdate
**,在它這個做用添加以前:
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
複製代碼
好,如今,我知道了**ClickCounter
在render階段中有哪些操做須要執行,那咱們來看看Fiber節點上這些操做改變的值。當React開始工做時,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) => {…}
}
},
...
}
}
複製代碼
工做結束以後,咱們獲得Fiber階段結果看起來這樣:
{
effectTag: 4,
elementType: class ClickCounter,
firstEffect: null,
memoizedState: {count: 1},
type: class ClickCounter,
stateNode: {
state: {count: 1}
},
updateQueue: {
baseState: {count: 1},
firstUpdate: null,
...
}
}
複製代碼
在更新執行以後,count
屬性的值在memoizedState
和updateQueue
中的baseState
上變成1
,React也更新了**ClickCounter
**組件實例中的state。
此刻,咱們在隊列中不在有更新,全部**firstUpdate
是null
,且重要的是,咱們修改了effectTag
屬性的值,它不在是0
,而是4
,二進制中爲100
,這表明第三位被設,而這一位表明Update
**的反作用tag(side-effect tag):
export const Update = 0b00000000100;
複製代碼
綜述,**ClickCounter
**Fiber節點的工做是,調用前置突變生命週期方法,更新state以及定義相關反作用。
一旦那些完成,React進入finishClassComponent,這個方法中,React調用組件實例**render
**方法,且對組件返回的孩子執行diff算法,這個文檔中有大體概述,這是相關的一部分:
當比較兩個相同類型的React DOM元素時,React觀察二者的屬性,保留DOM節點中一致的,且只更新變化的屬性。
而後若是咱們再深刻的話,咱們能夠知道它的確是比較Fiber節點和React元素,可是我如今就先不太詳細的說明了,由於這個處理至關細緻,我將會針對子協調單獨的寫文章分析。
若是你本身很好奇想知道這個細節,能夠查閱reconcileChildrenArray方法,由於在咱們的例子中,**
render
**方法返回React元素數組。
此刻,有兩個重要事情須要理解,首先,當React進行子協調處理時,它建立或更新了子React元素的Fiber節點,這些子元素有**render
方法返回,finishClassComponent
返回了當前Fiber節點的第一個孩子的引用,它將會賦值給nextUnitOfWork
,便於在工做循環中以後處理;其次,React更新孩子的props是其父級上執行工做的一部分,因此爲了作這個,它要使用render**方法返回的react元素上的數據。
例如,這裏是**span
元素相關的Fiber節點在React協調ClickCounter
**fiber以前的樣子:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 0},
...
}
複製代碼
正如你所見,memoizedProps
和pendingProps
上children
屬性值都是0
,這是**span
元素的render
**方法返回的React元素結構:
{
$$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
複製代碼
如你所見,Fiber節點和返回的React元素的props有點不一樣,在建立fiber節點副本的createWorkInProgress
方法中,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節點上的工做,它將拷貝它們到memoizedProps
**,並添加做用(effects)來更新DOM。
嗯,這就是在render階段中,React在**ClickCounter
Fiber節點上執行的全部工做,由於Button是ClickCounter
組件上的第一孩子,它將賦值給nextUnitOfWork
變量,因爲無事可作,因此React將會轉移到它的兄弟span
Fiber節點上,根據這裏描述的算法,這發生在completeUnitOfWork
**方法中。
因此,**nextUnitOfWork
變量如今指向span
副本,且React在它上面仍是工做,相似於在ClickCounte
**上的步驟,咱們開始於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
組件上子協調時,React更新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
方法,這個基本上是一個大的switch語句,相似於咱們在beginWork
**中看到的:
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
...
}
複製代碼
當工做完成以後,它看起來像:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 4,
updateQueue: ["children", "1"],
...
}
複製代碼
注意,effectTag
和updateQueue
字段的不一樣,它再也不是0
,而是**4
,二進制爲100
,這表明第三位被設,第三位表明着Update
的反作用tag,這是React在接下來的commit階段惟一須要作的工做,而updateQueue
**字段持有的負載(payload)將會在更新時用到。
一旦,React處理完**ClickCounter
和它們的孩子,它就完成了render
階段,它如今就能把完成成的副本(或者叫替代-alternate)樹賦值給FiberRoot
上的finishedWork
屬性。這是一顆新的須要刷新在屏幕上的樹,它能夠在render
**階段以後當即處理,或者掛起等瀏覽器給React空閒時間。
在咱們的例子中,由於span
節點和ClickCounter
組件都有反作用,React會把HostFiber
上的firstEffect
指向span
Fiber節點。
React在compliteUnitOfWork
方法中構建做用列表,這裏是帶有做用的Fiber樹,這些做用是更新**span
節點文本,調用ClickCounter
**的鉤子:
這裏是做用節點的線性列表:
這個階段開始於completeRoot方法,在它作任何工做以前,它把**FiberRoot
上的finishedWork
屬性設爲null
**:
root.finishedWork = null;
不像**render
階段,commit
階段老是同步的,因此它能夠安全的更新HostRoot
**來指示提交工做開始了。
**commit
階段就是React更新DOM以及調動後置突變生命週期方法componentDidUpdate
的地方,爲了這樣,它迭代在render
**階段建立的做用列表,並應用它。
咱們在**render
階段中,對span
和ClickCounter
**節點有以下做用:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
複製代碼
ClickCounter
的做用標籤值是5
或者二進制的101
,定義的**更新
工做被認爲是調用類組件的componentDidUpdate
生命週期方法。最低位也被設值,它表明這個Fiber節點在render
**階段的全部工做都已完成。
span
的做用標籤是4
或者二進制100
,定義的**更新
工做是host組件的DOM更新,在咱們例子中的span
元素,React將須要更新元素的textContent
**。
咱們來看React是如何應用這些做用的,commitRoot
方法,用於應用這些做用,有三個子方法組成:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
複製代碼
每一個子方法都會用循環來迭代做用列表,並檢查其中做用類型,當找到有關方面目的的做用時,就應用它。在咱們的例子中,它會調用**ClickCounter
組件的componentDidUpdate
生命週期方法,以及更新span
**元素上的文本內容。
第一個放commitBeforeMutationLifeCycles尋找**Snapshot
做用,且調用getSnapshotBeforeUpdate
方法,可是,由於咱們在ClickCounter
組件中沒有實現這個方法,因此React不會在render
**階段添加這個做用,因此在咱們的例子中,這個方法啥也沒作。
接着,React執行到commitAllHostEffects
方法,這裏,React就會把**span
元素的文本內容從0
修改到1
,而ClickCounter
**fiber上啥也不作,由於類組件的節點沒有任何DOM更新。
這個方法大體是,選擇正確做用類型,並應用相關的操做。在個人例子中,咱們須要更新**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
的負載(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更新被應用以後,React把**finishedWork
樹賦值給HostRoot
**,即把替換(alternate)樹設置爲當前樹:
root.current = finishedWork;
最後一個方法是commitAllLifecycles
方法,這裏React會調用後置突變生命週期方法。在**render
階段,React在ClickCounter
組件上添加Update
做用,這是commitAllLifecycles
方法尋找的做用之一,而後調用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
**方法。
額……講完啦。