[译] Fiber内幕:深入概述React新的协调算法
原文地址:medium.com/react-in-de…
如何以及為何從React組件到Fiber節(jié)點(diǎn)的一切內(nèi)容
React使用一個構(gòu)建用戶界面的JavaScript庫,它的核心機(jī)制是跟蹤組件狀態(tài)的的變化,然后將更新的狀態(tài)投影在屏幕上。在React中,我們把這個過程稱為協(xié)調(diào)。我們調(diào)用setState方法后,框架會檢測state和prop是否發(fā)生變化,并重新渲染UI組件。
React文檔關(guān)于這個機(jī)制提供了很好的高層面概述: React元素的角色,生命周期方法,render方法,以及應(yīng)用于子組件的diff算法。由render方法返回的不可變的React元素普遍被認(rèn)為是React的“虛擬DOM”。那個術(shù)語早期幫助React解釋給人們,但它也造成了一些困惑,也不再在React文檔中使用,這篇文章中,我會繼續(xù)稱之為React元素的樹。
除了React元素的樹,框架總還有用于保留狀態(tài)的內(nèi)部實(shí)例(組件,DOM節(jié)點(diǎn)等)的一棵樹。從16版本開始,React推出了內(nèi)部實(shí)例樹的新實(shí)現(xiàn),以及管理它的算法(代碼上稱為Fiber)。想要得知Fiber架構(gòu)帶來的好處,可以參見The how and why on React’s usage of linked list in Fiber。
這篇文章花費(fèi)了我很多時間,而且要是沒有 Dan Abramov! ?的個幫助,也不會講解地如此全面。
這是給你講解React內(nèi)部架構(gòu)系列的第一篇文章。這篇文章中,我想提供算法中重要概念和數(shù)據(jù)結(jié)構(gòu)的深度概述。一旦我們有足夠的背景,我們就可以探索這個算法以及用于遍歷和操作fiber樹的主要方法。系列中的下一篇文章將示范React如何使用這個算法來初始render以及操作state和props的更新,從那里我們將了解到調(diào)度(scheduler)的細(xì)節(jié)、子協(xié)調(diào)(child reconciliation)操作以及構(gòu)建更新鏈表(effect list)。
這里我將給你講述相當(dāng)高級的內(nèi)容,我保證你閱讀后可以理解到并發(fā)(Concurrent)React內(nèi)部工作背后的神奇。如果你想成為React的貢獻(xiàn)者的話,這個系列的文章也可以作為你的向?qū)?。我一個逆向代碼的虔誠者(就是喜歡死磕源碼),所以這里有很多關(guān)于React@16.6.0的資源鏈接。
這確實(shí)牽扯很多內(nèi)容,所以如果你沒有馬上理解也不必有很大壓力,一切都值得花時間。需要注意的是你不必了解這些來使用React,這篇文章是關(guān)于React如何內(nèi)部工作的。
設(shè)置一個背景
這里有個我們在整個系列中都會使用到的簡單應(yīng)用。我們有個button,簡單的增加數(shù)字,然后渲染到屏幕上。
這是實(shí)現(xiàn):
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};});}render() {return [<button key="1" onClick={this.handleClick}>Update counter</button>,<span key="2">{this.state.count}</span>]} } 復(fù)制代碼你可以在這里去執(zhí)行它。正如你看到的,它是一個簡單組件,通過render方法返回button和span兩個子組件。只要你點(diǎn)擊button,組件的狀態(tài)就會在處理器中更新,這繼而導(dǎo)致span元素中的text的更新。
React在**協(xié)調(diào)(reconciliation)**期間有執(zhí)行很多活動,例如,React在第一次render時執(zhí)行的操作,以及在我們這個簡單的應(yīng)用中狀態(tài)更新之后:
- 更新ClickCounter的state中的count參數(shù)
- 獲取和比較ClickCounter的子組件以及他們的props
- 更新span元素的props
在協(xié)調(diào)期間還執(zhí)行其他活動,像聲明周期方法或者更新refs。所有這些活動在Fiber架構(gòu)中統(tǒng)一起來被定義為一個“工作(work)”。工作的類型通常取決于React元素(element)的類型,例如,對于一個類組件(class component),React需要創(chuàng)建實(shí)例,而對于方法組件(function component)則不需要這樣。正如你所知,React中有很多種元素,如類組件、方法組件、host組件(DOM節(jié)點(diǎn))以及Portal等。元素的類型被定義在createElement方法中的第一個參數(shù),這個方法通常用在render方法中來場景一個元素。
在我們探索這些執(zhí)行的活動以及主要的Fiber算法時,我們先來對React內(nèi)部使用的數(shù)據(jù)結(jié)構(gòu)有個認(rèn)識。
從React元素到Fiber節(jié)點(diǎn)
React中每個組件是一個UI表示,我們可以叫它視圖(view)或者模板(template),它由render方法返回。這里便是我們ClickCounter的模板:
<button key="1" onClick={this.onClick}>Update counter</button> <span key="2">{this.state.count}</span> 復(fù)制代碼React元素(Elements)
一旦模板經(jīng)過JSX編譯,最終獲得一串React元素。這就是React組件的render方法真實(shí)返回的東西,而不是HTML。因?yàn)槲覀儧]有要求使用JSX,所以ClickCounter組件的render方法也可以寫成:
class ClickCounter {...render() {return [React.createElement('button',{key: '1',onClick: this.onClick},'Update counter'),React.createElement('span',{key: '2'},this.state.count)]} } 復(fù)制代碼render方法中的React.createElement調(diào)用可以創(chuàng)建兩個數(shù)據(jù)結(jié)構(gòu):
[{$$typeof: Symbol(react.element),type: 'button',key: "1",props: {children: 'Update counter',onClick: () => { ... }}},{$$typeof: Symbol(react.element),type: 'span',key: "2",props: {children: 0}} ] 復(fù)制代碼你可以看到React在這些對象上添加$$typeof當(dāng)作React元素的來唯一標(biāo)示,且我們還有type、key和props來描述這個元素,這些值由你傳給了React.createElement方法。這里注意,React是如何把文本內(nèi)容表達(dá)成span和button節(jié)點(diǎn)的孩子,click處理如何成為button元素props的一部分,這里還有React元素上其他一些字段如ref已經(jīng)超出了本文的范疇。
React元素ClickCounter沒有任何props或者key:
{$$typeof: Symbol(react.element),key: null,props: {},ref: null,type: ClickCounter } 復(fù)制代碼Fiber節(jié)點(diǎn)
在**協(xié)調(diào)(reconciliation)**期間,由render方法返回的每個React元素都將合并到fiber節(jié)點(diǎn)的樹中,每個React元素都有相對應(yīng)的fiber節(jié)點(diǎn),不像React元素,fiber不會在每次render時重新創(chuàng)建。這些可變的數(shù)據(jù)結(jié)構(gòu)帶有組件的狀態(tài)以及DOM。
我們之前討論的是框架根據(jù)React元素的類型來執(zhí)行不同的活動,在我們簡單的應(yīng)用中,對于類組件ClickCounter,它調(diào)用生命周期和render方法,而spanhost組件(DOM節(jié)點(diǎn))則執(zhí)行DOM變化,所以每個React元素轉(zhuǎn)化成類型相對應(yīng)的Fiber節(jié)點(diǎn),這些類型描述了需要完成的工作。
當(dāng)React元素首次被轉(zhuǎn)化成fiber節(jié)點(diǎn)時,React在createFiberFromTypeAndProps方法中使用這個元素中的數(shù)據(jù)創(chuàng)建fiber,在之后發(fā)生的更新中,React重用這個fiber節(jié)點(diǎn),且通過相對應(yīng)的React元素中數(shù)據(jù)更新必要的屬性。
React也可能需要基于key屬性在層級中移動節(jié)點(diǎn),或者如果對應(yīng)的React元素不再由render方法返回時,則刪除掉它。
找出ChildReconciler方法,你可以看到所有活動的列表,以及React在當(dāng)前存在fiber節(jié)點(diǎn)上執(zhí)行的對應(yīng)方法。
因?yàn)镽eact為每個React元素都創(chuàng)建了一個fiber,所以只要我們有這些元素的一棵樹,那我們就會有fiber節(jié)點(diǎn)的一棵樹。在我們簡單應(yīng)用案例中,它看起來如下:
所有fiber節(jié)點(diǎn)通過一個鏈表鏈接起來,這個鏈表使用了fiber節(jié)點(diǎn)中屬性:child、sibling和return。關(guān)于如何和為何這種方式,可以查閱我的文章The how and why on React’s usage of linked list in Fiber。
當(dāng)前和正在執(zhí)行的樹(Current and work in progress trees)
在第一次渲染(render)之后,React最后得到了一顆fiber樹,它反映了用于渲染UI的應(yīng)用的狀態(tài),這顆樹被當(dāng)作current。當(dāng)React開始處理更新時,它構(gòu)建所謂的workInProgress樹來反映將來刷新屏幕的狀態(tài)。
所有工作都在來自workInProgress樹的fiber上執(zhí)行。當(dāng)React經(jīng)過當(dāng)前樹時,對于每一個先存在的fiber節(jié)點(diǎn),它都會創(chuàng)建一個替代(alternate)節(jié)點(diǎn),這些節(jié)點(diǎn)組成了workInProgress樹。這個節(jié)點(diǎn)是使用render方法返回的React元素的數(shù)據(jù)創(chuàng)建的。一旦更新處理完以及所有相關(guān)工作完成,React就有一顆替代樹來準(zhǔn)備刷新屏幕。一旦這顆workInProgress樹渲染(render)在屏幕上,它便成了當(dāng)前樹。
React的設(shè)計(jì)原則之一是連貫性。React總是一次性更新DOM,而不是只顯示部分結(jié)果。這顆workInProgress樹為當(dāng)做是‘草稿’,它對用戶是不可見的,以至于React可以先處理所有組件,然后再刷新他們的改變到屏幕上。
在這個代碼中,可以看到很多方法,這些方法持有來著current和workInProgress樹的fiber節(jié)點(diǎn),這是一個這樣方法的簽名:
function updateHostComponent(current, workInProgress, renderExpirationTime) {...} 復(fù)制代碼每個fiber節(jié)點(diǎn)中的alternate字段持有它的一個副本,這個副本節(jié)點(diǎn)表示current樹指向workInProgress樹的,反之亦然,代碼如下:
function createWorkInProgress(current, ...) {let workInProgress = current.alternate;if (workInProgress === null) {workInProgress = createFiber(...);}...workInProgress.alternate = current;current.alternate = workInProgress;...return workInProgress; } 復(fù)制代碼副作用(side-effects)
我們可以認(rèn)為React中組件是使用state和props的方法,用于計(jì)算UI展示。每個其他活動,像DOM變化或者調(diào)用生命周期方法,應(yīng)當(dāng)認(rèn)為是一個副作用,或者一個簡單的作用。作用(Effects)也在這個文檔中提及。
你以前可能做過請求數(shù)據(jù),訂閱,或者在React組件中手動修改DOM,我們把這些操作叫做副作用(或者簡說作用),因?yàn)樗鼈儠绊懫渌M件,且不能在渲染時完成。
你可以看到很多state和props是如何造成副作用的,
既然應(yīng)用副作用是一個工作的類型,那一個fiber節(jié)點(diǎn)就是除了更新之外還用于跟蹤作用的簡明機(jī)制。每一個fiber節(jié)點(diǎn)可以有很多關(guān)聯(lián)的作用,它們被編碼到effectTag字段中(effectTag使用位運(yùn)算的妙處啦)。
所以Fiber中的作用(effects)基本上定義了一個組件實(shí)例在其更新操作之后需要完成的工作(work),對于host組件(DOM元素),這個工作包括更新、添加和刪除元素;對于類組件,React可能需要更新refs,以及調(diào)用componentDidMount和componentDidUpdate生命周期方法。這里當(dāng)然還有其他一些與fiber類型相對應(yīng)的作用。
作用列表(Effects list)
React處理更新很快,為了實(shí)現(xiàn)這個層次的性能,它采用了個別有趣的技巧,比如,將含有作用的fiber節(jié)點(diǎn)用線性列表表示,從而可以快速迭代。迭代線性列表比樹要快,且可以不必花時間在沒有副作用的節(jié)點(diǎn)上。
這個列表的目的是用于標(biāo)記一些節(jié)點(diǎn),這些節(jié)點(diǎn)有DOM更新或者與其關(guān)聯(lián)的副作用。這個列表是finishedWork的子集,且通過nextEffect屬性鏈接起來,而不是current和workInProgress樹中使用的child屬性。
Dan Abramov對作用列表作了一個類比,就像一顆圣誕樹中通過“圣誕燈”來把所有作用節(jié)點(diǎn)連接起來。為了虛擬化它,我們設(shè)想以下這顆fiber樹,其中點(diǎn)亮的節(jié)點(diǎn)有一些工作要做,例如,我們更新使得c2插入DOM中、d2和c1改變屬性,以及d2觸發(fā)生命周期方法,那作用列表就講它們連接起來,以至于React之后可以濾過其他節(jié)點(diǎn):
你可以看到含有作用的節(jié)點(diǎn)和如何連接起來。當(dāng)要遍歷這些節(jié)點(diǎn)時,React使用firstEffect得出列表從哪里開始,那上述的示意圖可以用線性列表如下表示:
正如你所見,React執(zhí)行作用的順序是從子向上到父的。
Fiber樹的根節(jié)點(diǎn)(Root of the fiber tree)
每個React應(yīng)用有一個或多個DOM元素作為容器,在我們的例子中,它是ID是container的div元素:
const domContainer = document.querySelector('#container'); ReactDOM.render(React.createElement(ClickCounter), domContainer); 復(fù)制代碼React為這些容器的每個創(chuàng)建一個fiber根節(jié)點(diǎn),你可以通過DOM元素的引用訪問它:
const fiberRoot = query('#container')._reactRootContainer._internalRoot 復(fù)制代碼這個fiber根節(jié)點(diǎn)就是React持有fiber樹引用的地方,它保存在fiber根節(jié)點(diǎn)的current屬性上:
const hostRootFiberNode = fiberRoot.current 復(fù)制代碼fiber樹開始于HostRoot的fiber節(jié)點(diǎn)的一個特殊類型,它由內(nèi)部創(chuàng)建并將頂層組件作為父節(jié)點(diǎn)。這里有一個通過從stateNode屬性從HostRootfiber節(jié)點(diǎn)返回到FiberRoot的連接:
fiberRoot.current.stateNode === fiberRoot; // true 復(fù)制代碼你可以通過fiber根節(jié)點(diǎn)獲取HostFiber節(jié)點(diǎn)來探索fiber樹,或者你可以像這樣從組件實(shí)例中獲取獨(dú)立的fiber節(jié)點(diǎn):
compInstance._reactInternalFiber 復(fù)制代碼Fiber節(jié)點(diǎn)結(jié)構(gòu)
我們來看一下由ClickCounter組件創(chuàng)建的fiber節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu):
{stateNode: new ClickCounter,type: ClickCounter,alternate: null,key: null,updateQueue: null,memoizedState: {count: 0},pendingProps: {},memoizedProps: {},tag: 1,effectTag: 0,nextEffect: null } 復(fù)制代碼以及spanDOM元素:
{stateNode: new HTMLSpanElement,type: "span",alternate: null,key: "2",updateQueue: null,memoizedState: null,pendingProps: {children: 0},memoizedProps: {children: 0},tag: 5,effectTag: 0,nextEffect: null } 復(fù)制代碼fiber節(jié)點(diǎn)中有許多字段,我已經(jīng)在前面有描述字段alternate、effectTag和nextEffect的目的,現(xiàn)在我看看為何還需要其他字段。
stateNote
持有類組件實(shí)例、DOM節(jié)點(diǎn)或者其他與這個fiber節(jié)點(diǎn)關(guān)聯(lián)的React元素類型的引用,一般來說,我們可以說這個屬性用來持有與fiber相關(guān)的本地狀態(tài)。
type
定義與這個fiber關(guān)聯(lián)的方法或者類,對于類組件,它指向類的構(gòu)造方法;對于DOM元素,它具體為HTML標(biāo)簽;我經(jīng)常用這個字段來理解fiber節(jié)點(diǎn)關(guān)聯(lián)的是什么元素。
tag
定義了fiber的類型,這個用來在協(xié)調(diào)算法中定義那些工作需要完成。如之前所說,工作的不同取決于React元素類型,方法createFiberFromTypeAndProps映射了一個React元素到相對應(yīng)fiber節(jié)點(diǎn)類型。在我們的例子應(yīng)用中,ClickCounter組件的tag屬性值為1,代表了ClassComponent,以及span組件的是5,代表了HostComponent。
updateQueue
一個包括狀態(tài)更新、callbacks以及DOM更新的隊(duì)列。
memoizedState
fiber中用于創(chuàng)建輸出的狀態(tài),當(dāng)處理更新時,它反映了當(dāng)前已經(jīng)渲染在屏幕上的狀態(tài)。
memoizedProps
fiber中在前一次渲染時用于創(chuàng)建輸出的props。
pendingProps
由React元素中的新數(shù)據(jù)而來的已經(jīng)更新過的props,且需要應(yīng)用于子組件或者DOM元素。
key
一組子組件的唯一標(biāo)示,用于React得出列表中哪個改變了、添加了或者刪除了。這個與React中在這里描述的的“列表與key”的功能相關(guān)。
你可以從這里得到fiber節(jié)點(diǎn)的整個數(shù)據(jù)結(jié)構(gòu)。我濾過了一些上面解釋過的字段。特別是我跳過了**child、sibling和return,這些在我前一篇文章中介紹過了。還有一類字段像expirationTime、childExpirationTime以及mode特定用于調(diào)度(Scheduler)**。
整體算法
React執(zhí)行工作主要有兩個階段:render 和 commit。
在第一個render階段,React執(zhí)行由setState或者React.render調(diào)度的組件上的更新,且得出在UI上哪些需要被更新,如果它是初始渲染,那React會為每個有render方法返回的元素創(chuàng)建一個新的fiber節(jié)點(diǎn),在后續(xù)的更新中,當(dāng)前存在React元素的fiber會被重用和更新。這個階段的結(jié)果是一顆fiber節(jié)點(diǎn)被標(biāo)記副作用的樹。這些作用被描述為在接下來的commit階段中需要完成的工作,在這個階段,React取標(biāo)記作用的fiber樹并把它們應(yīng)用到實(shí)例上,遍歷作用列表且執(zhí)行DOM更新以及其他用戶可見的變化。
理解在第一個render階段中執(zhí)行的工作可以是異步的很重要。React在可用的時間內(nèi)能處理一個或多個fiber節(jié)點(diǎn),然后停止來保存完成的工作并妥協(xié)于一些事件(比如優(yōu)先級高的UI事件),它之后可以在之前離開的方法在繼續(xù)執(zhí)行,然而有時可能會丟棄已完成的工作,并從頂層重來。由于這個階段的執(zhí)行的工作不會導(dǎo)致用戶可見的變化(如DOM更新),所以這個暫停是可行的。不同的是,接下來的commit階段總是同步的,因?yàn)檫@個階段的執(zhí)行的工作會導(dǎo)致用戶可見的變化,這也是為什么React一把完成它們的原因。
調(diào)用生命周期方法是React執(zhí)行的一種工作類型,一些方法執(zhí)行在render階段,一些執(zhí)行在commit階段,下面是在render階段中執(zhí)行的生命周期方法列表:
- [UNSAFE_]componentWillMount (deprecated)
- [UNSAFE_]componentWillReceiveProps (deprecated)
- getDerivedStateFromProps
- shouldComponentUpdate
- [UNSAFE_]componentWillUpdate (deprecated)
- render
正如你所見,一些在render階段中被遺留的方法從16.3版本開始被標(biāo)記為UNSAFE,它們在16.x的release版本中被棄用掉了,而它們不帶UNSAFE前綴的副本在17.0中將被移除,你可以在這里關(guān)于這些改變,以及遷移建議的內(nèi)容。
你好奇這個的原因嗎?
那,我們已經(jīng)得知**render階段不會造成副作用(如DOM更新),且React可以對組件異步處理更新(且在的說,甚至可以在多線程中執(zhí)行)。然后被標(biāo)記了UNSAFE的聲明周期總是被誤解或者不易察覺的誤用,開發(fā)者傾向于把有副作用的邏輯放在這些方法中,這在新的異步渲染策略中可能會導(dǎo)致一些問題。盡管只有他們未標(biāo)記UNSAFE**前綴的副本被移除掉了,但它們?nèi)匀豢赡茉谖磥淼牟l(fā)模型(Concurrent Mode)中造成問題,當(dāng)然這個模式你可以不啟用。
這里是**commit**階段執(zhí)行的生命周期方法列表:
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
因?yàn)檫@些方法在同步的**commit**階段執(zhí)行,所有它們可以包含副作用以及觸控DOM。
好,我們現(xiàn)在已經(jīng)有了一定的基礎(chǔ)去看看用于遍歷樹和執(zhí)行工作的算法。
Render階段
這個協(xié)調(diào)算法總是開始于頂層的HostRootfiber節(jié)點(diǎn),這個節(jié)點(diǎn)由renderRoot方法創(chuàng)建,然而React能夠跳過已經(jīng)處理過的fiber節(jié)點(diǎn),直到它找到尚未完成工作的節(jié)點(diǎn),例如,如果你在一個組件樹深處調(diào)用setState,React將從頂部開始,但是很快就跳過一些節(jié)點(diǎn),找到調(diào)用setState方法的組件。
工作循環(huán)(work loop)中的主要步驟
所有的fiber節(jié)點(diǎn)都會在work loop做處理,這里是這個循環(huán)的同步部分的實(shí)現(xiàn):
function workLoop(isYieldy) {if (!isYieldy) {while (nextUnitOfWork !== null) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);}} else {...} } 復(fù)制代碼在上面代碼中,nextUnitOfWork持有一個fiber節(jié)點(diǎn),這個節(jié)點(diǎn)來自還有一些工作需要做的workInProgress樹,正如React遍歷fiber樹一樣,它使用這個變量來知道是否還有其他未完成工作的fiber節(jié)點(diǎn),當(dāng)前fiber節(jié)點(diǎn)處理之后,這個遍歷將要么獲取下一個fiber節(jié)點(diǎn)的引用,要么為null,在**null**的情況下,React將退出工作循環(huán),并準(zhǔn)備提交更新。
這里有4個主要的方法,用于遍歷樹,以及初始化或者完成工作:
- performUnitOfWork
- beginWork
- completeUnitOfWork
- completeWork
為了示例它們是怎么用的,看看下面遍歷fiber樹的動畫。我以及用demo把這些方法做了個簡單是的實(shí)現(xiàn),每個方法都會取fiber節(jié)點(diǎn)來處理,正如React沿著樹往下走時,你可以看到當(dāng)前活躍fiber節(jié)點(diǎn)的變化,這個視頻中你可以清晰地看到算法是如何從一個樹枝走到另一個樹枝的,它在移動到父節(jié)點(diǎn)前,首先得先完成子節(jié)點(diǎn)的工作。
注意:垂直直線連接代表兄弟,拐彎連接代表父子,如**b1沒有子節(jié)點(diǎn),而b2有一個c1**孩子。
這是視頻連接,其中你可以暫停播放,查看當(dāng)前節(jié)點(diǎn)和方法的狀態(tài)。概念上,你可以把“begin”當(dāng)作進(jìn)入組件,把“complete”當(dāng)作離開組件,你也可以在這里執(zhí)行這個例子和實(shí)現(xiàn),正如我解釋這些方法所做的事情。
我們從頭兩個方法**performUnitOfWork和beginWork**開始:
function performUnitOfWork(workInProgress) {let next = beginWork(workInProgress);if (next === null) {next = completeUnitOfWork(workInProgress);}return next; }function beginWork(workInProgress) {console.log('work performed for ' + workInProgress.name);return workInProgress.child; } 復(fù)制代碼performUnitOfWork方法從workInProgress中接受一個fiber節(jié)點(diǎn),調(diào)用beginWork方法來開始工作。fiber節(jié)點(diǎn)上需要執(zhí)行的所有活動都將從這個方法開始,對于這個示例的目的,我們只打印一下組件名稱,就當(dāng)是工作已經(jīng)完成了。beginWork總是返回一個指針,指向循環(huán)中要處理的下一個子節(jié)點(diǎn),或者指向null。
如果有下一個子節(jié)點(diǎn),它會在**workLoop方法中賦值給nextUnitOfWork變量,然后,如果沒有子節(jié)點(diǎn),React知道到達(dá)了樹枝的末尾,所有就可以完成(complete)當(dāng)前這個節(jié)點(diǎn)。一個節(jié)點(diǎn)只要完成了,它就會需要從兄弟和父級節(jié)點(diǎn)繼續(xù)執(zhí)行工作,這在completeUnitOfWork**方法中進(jìn)行:
function completeUnitOfWork(workInProgress) {while (true) {let returnFiber = workInProgress.return;let siblingFiber = workInProgress.sibling;nextUnitOfWork = completeWork(workInProgress);if (siblingFiber !== null) {// If there is a sibling, return it// to perform work for this siblingreturn siblingFiber;} else if (returnFiber !== null) {// If there's no more work in this returnFiber,// continue the loop to complete the parent.workInProgress = returnFiber;continue;} else {// We've reached the root.return null;}} }function completeWork(workInProgress) {console.log('work completed for ' + workInProgress.name);return null; } 復(fù)制代碼你可以從中看到方法大致是一個大的**while循環(huán),React當(dāng)workInProgress節(jié)點(diǎn)沒有孩子時就進(jìn)入這個方法。在完成當(dāng)前fiber的工作后,它檢查是否有還有兄弟,如果有,React退出這個方法,并返回指向兄弟的指針,它將會賦值給nextUnitOfWork**變量,然后React將通過兄弟節(jié)點(diǎn)在新樹枝上開始執(zhí)行工作。重要的是明白這種情況中React只有是前面的兄弟節(jié)點(diǎn)完成了工作,而它還沒有完成父節(jié)點(diǎn)的工作,只有所有開始于子節(jié)點(diǎn)的樹枝上的工作完成了,它才算是為父節(jié)點(diǎn)完成了工作,然后原路返回。
正如你從實(shí)現(xiàn)中所見,**performUnitOfWork和completeUnitOfWork方法的目的幾乎是迭代,而主要活動發(fā)生在beginWork和completeWork方法中。在這個系列接下來的文章中,我們將知道,當(dāng)React進(jìn)入beginWork和completeWork方法中時,ClickCounter組件和span**節(jié)點(diǎn)會發(fā)生什么。
Commit階段
這個階段開始于completeRoot方法,這里React便會更新DOM,以及調(diào)用前前后置突變生命周期方法。
當(dāng)React進(jìn)入這個階段時,它有兩顆樹和一個作用列表,第一顆樹表示了當(dāng)前渲染在屏幕上的狀態(tài),而這里還有在**render階段構(gòu)建的一顆替代樹,它調(diào)用代碼中finishedWork和workInProgress,表示需要在屏幕上反應(yīng)出來的狀態(tài),這顆替代樹鏈接方式類似當(dāng)前樹,通過child和sibling**指針鏈接。
還有作用列表——通過**nextEffect連接起來的finishedWork樹的節(jié)點(diǎn)子集。記住作用列表是在render**階段生成,整個渲染(rendering)的要點(diǎn)就是得出哪些節(jié)點(diǎn)需要插入、更新、刪除,以及哪些組件需要執(zhí)行它們的生命周期方法,這便是作用列表要告訴我們的,這是會在commit階段中被迭代的節(jié)點(diǎn)集合。
為了debugging,當(dāng)前樹可以通過fiber根節(jié)點(diǎn)**current屬性方法,finishedWork樹可以通過當(dāng)前樹上的HostFiber節(jié)點(diǎn)的alternate**來訪問。
主要運(yùn)行在commit階段的方法是commitRoot,大致如下操作:
- 標(biāo)記了**Snapshot作用的節(jié)點(diǎn)執(zhí)行getSnapshotBeforeUpdate**生命周期方法。
- 標(biāo)記了**Deletion作用的節(jié)點(diǎn)執(zhí)行componentWillUnmount**生命周期方法。
- 執(zhí)行所有DOM的插入、更新和刪除。
- 把**finishedWork**樹置為當(dāng)前樹。
- 標(biāo)記了**Placement作用的節(jié)點(diǎn)執(zhí)行componentDidMount**生命周期方法。
- 標(biāo)記了**Update作用的節(jié)點(diǎn)執(zhí)行componentDidUpdate**生命周期方法。
調(diào)用前置突變方法**getSnapshotBeforeUpdate之后,React提交了樹中所有副作用。它以兩個步驟來做,第一步是執(zhí)行所有DOM(host)的插入、更新和刪除以及ref的卸載,然后React把finishedWork樹賦值給FiberRoot,即當(dāng)workInProgress樹為current樹,這在commit階段的第一步和第二步之間執(zhí)行,便于之前的樹在componentWillUnmount是還是當(dāng)前樹,而在componentDidMount/Update**時,完成樹(finished work)為當(dāng)前樹。在第二步中,React調(diào)用其他所有生命周期方法和ref回調(diào),這些方法在單獨(dú)步驟中執(zhí)行,以致整個樹中所有的替換、更新和刪除已經(jīng)被調(diào)用。
這里運(yùn)行上述描述方法的大意:
function commitRoot(root, finishedWork) {commitBeforeMutationLifecycles()commitAllHostEffects();root.current = finishedWork;commitAllLifeCycles(); } 復(fù)制代碼每個子方法都實(shí)現(xiàn)了一個循環(huán)來迭代作用列表以及檢查作用類型,當(dāng)發(fā)現(xiàn)和這個方法目的有關(guān)的作用,就應(yīng)用它。
前置突變(Pre-mutation)生命周期方法
例如這里的一個代碼,迭代作用樹,并檢查一個節(jié)點(diǎn)是否是**Snapshot**作用:
function commitBeforeMutationLifecycles() {while (nextEffect !== null) {const effectTag = nextEffect.effectTag;if (effectTag & Snapshot) {const current = nextEffect.alternate;commitBeforeMutationLifeCycles(current, nextEffect);}nextEffect = nextEffect.nextEffect;} } 復(fù)制代碼對于類組件來說,這個作用意味著調(diào)用**getSnapshotBeforeUpdate**方法。
DOM更新
commitAllHostEffects是React執(zhí)行DOM更新的方法,這個方法定義了節(jié)點(diǎn)需要完成操作的類型,且執(zhí)行它:
function commitAllHostEffects() {switch (primaryEffectTag) {case Placement: {commitPlacement(nextEffect);...}case PlacementAndUpdate: {commitPlacement(nextEffect);commitWork(current, nextEffect);...}case Update: {commitWork(current, nextEffect);...}case Deletion: {commitDeletion(nextEffect);...}} } 復(fù)制代碼有趣的是,React在刪除操作中,把**commitDeletion方法中調(diào)用componentWillUnmount**方法當(dāng)作其中一部分。
后置突變(Post-mutation)生命周期方法
commitAllLifecycles是React調(diào)用所有剩余生命周期方法**componentDidUpdate和componentDidMount**的方法。
這里我們就講完了。
轉(zhuǎn)載于:https://juejin.im/post/5cdb5b205188252035420c7f
總結(jié)
以上是生活随笔為你收集整理的[译] Fiber内幕:深入概述React新的协调算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Anordighos手绘休闲脚本字体
- 下一篇: 一看就懂的极简MVVM