React是用於創建用戶交互界面的JavaScript庫。它的核心機制是跟蹤組件的狀態而且更新顯示到屏幕上,這個過程被稱爲協調(reconciliation)當組件的state或者props發生改變時,咱們使用setState方法而且進行檢查,從新渲染UI。javascript
React的文檔提供了對這個機制的講解:React元素,生命週期函數和render方法的做用,以及diff算法在子組件的應用。由render函數返回的react元素被稱爲「virtual DOM」。這個詞早起常被用來解釋react的工做原理,可是這個詞常常引發誤解,而且已經不被react的官方文檔所使用。在這篇文章我會繼續用它表示react的元素樹。html
下面是一個簡單的點擊按鈕增長數字的組件java
代碼以下:node
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> ] } }
正如你所見,這是一個簡單的組件,返回button
和span
兩個子元素。只要一點擊按鈕,組件的狀態就會更新,反過來組件德狀態就會更新到span
元素。
在這步協調算法中react進行了多個步驟。好比下面是在第一次渲染和狀態更新之間的React高級步驟:react
span
元素的props在協調算法階段還有其餘活動如生命週期函數和更新refs。全部的這些步驟在Fiber中統一稱爲「工做(work)」。這些工做的種類一般取決於React元素的種類。好比:對於類組件,React須要建立一個實例,而函數式組件則不須要。正如你所知,在React中有許多元素類型,如類組件和函數式組件,主機元素(host elements)和portals等等。React組件的類型由創造組件時函數第一個單詞決定。這個函數一般是在render函數中建立元素。
在咱們開始探索fiber算法的前,先熟悉一下React內部的數據結構。算法
Every component in React has a UI representation we can call a view or a template that’s returned from the render method. Here’s the template for our ClickCounter component:
React的每一個組件都有一個由render函數返回的咱們稱爲視圖或者模板的UI。下面是ClickCounter組件的模板:安全
<button key="1" onClick={this.onClick}>Update counter</button> <span key="2">{this.state.count}</span>
當一個模板進入JSX的編譯器後,輸出的是一些React元素。render函數返回的是就是這些而不是HTML。當咱們不須要使用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 ) ] } }
在render函數中React.createElement
會創造兩個像下面這樣的數據結構:數據結構
[ { $$typeof: Symbol(react.element), type: 'button', key: "1", props: { children: 'Update counter', onClick: () => { ... } } }, { $$typeof: Symbol(react.element), type: 'span', key: "2", props: { children: 0 } } ]
你能夠看到React經過添加$$typeof
到這些對象,是這些對象變爲可識別的React元素。接下來咱們就有了屬性的類別,key和props來描述這個元素。這些值來自於你傳給React.createElement
函數的。請注意React是怎麼把文字內容表現爲span和button節點的子元素的。點擊事件時怎麼成爲button元素的props的。React還有其餘屬性像refs,但這些超過了本文章討論的內容。
The React element for ClickCounter doesn’t have any props or a key:
ClickCounter不會擁有任何的props或者key:多線程
{ $$typeof: Symbol(react.element), key: null, props: {}, ref: null, type: ClickCounter }
在協調算法階段,從render方法中返回的每個React元素合併成一個fiber節點樹。每個React元素都有與之對應的fiber節點。跟React元素不一樣的是,fiber並非每次render都會從新建立的。fiber就是保持組件狀態和DOM結構的可變的數據結構。
咱們先前討論過React根據元素的種類的不一樣表現不一樣的活動。在咱們的簡單的應用中,對於類組件咱們稱ClickCounter爲生命週期方法。對於render方法,span元素組件提供了Dom變化的功能。因此每個React元素轉化成描述須要執行的活動的Fiber節點。fiber結構同時也對追蹤,安排,暫停和遺棄這些功能提供了方便的方法。
當React元素第一次轉化成fiber節點時,React在createFiberFromTypeAndProps
方法中將元素的數據編譯成fiber。在接下來的更新中,React會重複使用fiber節點,並更新須要更新的元素的對應的fiber節點。React還會根據key轉化節點層級或者刪除在render方法中沒有返回的React元素的對應節點。
由於React的每一個fiber都有對應的React元素,同時又有這些元素組成的樹,因此咱們也有fiber樹,在咱們簡單的應用中它是這樣的:
全部的fiber節點經過由child,sibling和return組成的fiber節點鏈接的列表。
在首次渲染以後,React生成一顆表示應用狀態並用於渲染UI的fiber樹。這顆樹一般被稱爲current。當React開始進行更新工做時會生成一顆稱爲workInProgress的樹,這顆樹表示將要顯示到屏幕上的狀態。
fibers上展現的全部的工做都是來自於workInProgress樹的。當React檢查current樹,每一個存在的fiber節點都會生成一個替代的節點,這些節點構成workInProgress樹這些節點是由render函數返回的React元素生成的。一旦更新和相關的工做都完成了,React將會有另外一顆樹準備顯示到屏幕上。當workInProgress樹顯示到屏幕上時就變成了current樹。
React的核心準則之一就是連續性。React一般是一次性更新DOM的——它不會顯示部分結果。workInProgress樹不會在用戶前顯示就如同是草稿同樣,因此React能夠先對全部的組件進行檢查更新,而後改變DOM結構。
In the sources you’ll see a lot of functions that take fiber nodes from both the current and workInProgress trees. Here’s the signature of one such function:
在源代碼裏面你能夠看到有許多方法會從current樹和workInProgress樹上拿取fiber節點。下面就是這樣的一個方法:
function updateHostComponent(current, workInProgress, renderExpirationTime) {...}
咱們能夠把React組件想象爲用來計算state和props並展現UI界面的函數。任何其餘的像改變DOM和使用生命週期函數能夠被稱做反作用,或者簡單的稱爲做用。做用也在文檔裏被說起:
你可能以前經過網絡獲取或者訂閱獲取數據,又或者在React組件裏手動的改變DOM。咱們稱之爲反作用,由於他們會影響其餘的組件而且不能再渲染期間完成。
你能夠看到大部分的state和props是如何由於更新產生反作用的。因爲使用反作用也是工做的一種,fiber節點也是更重反作用的有效機制。每個節點能夠擁有與他聯繫的反作用,他們被編碼到一個叫作做用標籤(effectTag)的地方。
因此Fiber裏的反作用定義了在更新完成以後實例須要作的工做。對於Dom元素這些工做由增長,更新或者刪除元素組成。對於類組件,則是須要更新refs和發起componentDidMount 和componentDidUpdate 生命週期函數。還有其餘類型的fibers聯繫的反作用。
React更新過程很是快,React經過採起一些有趣的技術到達這樣的高性能。其中之一就是建立了一個帶effects的線性列表的fiber節點,能夠快速迭代。迭代線性列表比樹結構快的多,不須要花費時間在沒有反作用的節點上了。
這個列表的目的是標記具備DOM更新和其餘做用相聯繫的節點。這個列表是finishedWork tree
的子集,而且使用 nextEffect
屬性代替在current和workInProgress 樹種使用的子屬性。
Dan Abramov 對effect樹舉了個例子。他喜歡把effect樹想象爲一顆聖誕樹,用聖誕燈把全部的effect節點聯繫起來。讓咱們來看下面這張圖,高亮顯示的是須要工做的fiber節點。好比,咱們的更新把c2插入到Dom結構中,改變d2和c1的屬性,觸發b2的生命週期的屬性。effect列表會把他們連起來,那麼react後面的處理就能夠跳過其餘節點了:
你能夠看到有做用的節點是如何連在一塊兒的。爲了遍歷這些節點,React使用firstEffect來指出這個列表從哪裏開始,就像下面這樣:
Every React application has one or more DOM elements that act as containers. In our case it’s the div element with the ID container.
每個React應用都有一個或者多個Dom元素用來做爲容器。在咱們的例子就是帶有ID屬性的div元素。
const domContainer = document.querySelector('#container'); ReactDOM.render(React.createElement(ClickCounter), domContainer);
React creates a fiber root object for each of those containers. You can access it using the reference to the DOM element:
React爲每個容器都建立了一個fiber根對象。你能夠經過這些參考看到這個DOM元素:
const fiberRoot = query('#container')._reactRootContainer._internalRoot
這個fiber跟對象就是React控制fiber樹的參考。它保存在fiber根對象中的current屬性中。
const hostRootFiberNode = fiberRoot.current
fiber樹開始於一個特別的fiber種類就是HostRoot。它有內部建立而且就像是其餘組件的父元素。HostRoot元素節點能夠經過stateNode屬性返回FiberRoot:
fiberRoot.current.stateNode === fiberRoot; // true
你能夠經過fiber根對象探索fiber樹,或者能夠對一個組件的fiber節點像下面那樣操做:
compInstance._reactInternalFiber
Let’s now take a look at the structure of fiber nodes created for the ClickCounter component
讓咱們看看有ClickCounter組件生成的fiber節點的結構吧:
{ stateNode: new ClickCounter, type: ClickCounter, alternate: null, key: null, updateQueue: null, memoizedState: {count: 0}, pendingProps: {}, memoizedProps: {}, tag: 1, effectTag: 0, nextEffect: null }
下面是span
Dom元素的:
{ 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 }
在fiber節點中有不少屬性,我已經在以前講述了alternate
,effectTag
和nextEffect
,讓咱們看看其餘的屬性的做用:
createFiberFromTypeAndProps
函數映射了React元素對fiber種類。在咱們的應用中,ClickCounte的tag屬性是1,這表明是類組件而span元素是5表明了HostComponent(須要改變外觀的組件)。你能夠在這裏發現一個複雜的節點, 我已經省略了先前已經解釋過的一些屬性。而像expirationTime
, childExpirationTime
和mode
與調度有關。
React的工做能夠分爲兩個階段: render和commit。
在第一個render階段,React組件經過setState和React.render方法分辨哪些須要在UI中更新。若是是首次渲染,則React會爲每個從render函數中返回的元素創造一個新的fiber節點。React會對於其餘的已經存在的React元素再次使用並更新。這個階段的結果就是生成一顆帶有反作用的fiber節點數。這個階段的描述的effetcs要在接下來的commit階段完成。
咱們要明白在render階段的工做能夠是異步的。React能夠根據可用的時間來進行一個或多個工做單元,而後中止並保存已完成的工做單元,進行其餘的事件處理。當其餘的事件處理完成後再執行剩下的工做單元。不過有時候,可能須要丟棄已經完成的工做單元,從頭開始。這些暫停使得界面能夠對用戶的操做進行反應,例如dOM更新。然而,commit階段則是同步的,這是由於這個階段的工做改變了呈現給用戶的界面。
Calling lifecycle methods is one type of work performed by React. Some methods are called during the render phase and others during the commit phase. Here’s the list of lifecycles called when working through the first render phase:
生命週期函數就是React的一種工做類型。一些方法在render階段被使用,另一些在commit階段被使用。下面是在render階段使用的生命週期函數的列表:
正如你所見,從16.3版本開始一些之前的生命週期函數在render階段被標記爲不安全。它們如今在文檔中稱爲之前(legacy )的生命週期函數。
你對這其中的緣由好奇嘛?
就像咱們剛剛學習的所說的,在render階段並不產生像DOM更新這樣的反作用,React能夠進行異步更新(甚至能夠多線程工做)。然而在上面標記不安全的生命週期函數一般被誤解以及被不合理的使用。開發者一般把有反作用的代碼放到這些函數中,可能引發新的異步的渲染的問題。
下面是在commit階段執行的生命週期函數:
由於在這些函數在commit階段同步執行,因此他們包含了反作用而且與DOM密切相關。
協調算法老是用renderRoot函數從最開始的HostRott fiber節點開始。然而React會跳開已經處理過的fiber節點知道發現有未處理的工做單元。好比,你在組件樹的深處使用setState,React會從第一個組件開始,可是會快速的跳過父組件達到調用setState方法的組件。
全部的fiber節點都在工做循環中進行。下面是一個同步循環的實現:
function workLoop(isYieldy) { if (!isYieldy) { while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } else {...} }
在上面的代碼中,nextUnitOfWork 保存了從workInProgress 樹中的fiber節點要作的工做。當react遍歷fiber樹的時候,它使用這個變量來發現是否有另外有未完成工做的fiber節點。噹噹前的fiber節點運行結束時,這個變量要麼保存了下個fiber節點或者null。到那個時候React準備進行commit了。
There are 4 main functions that are used to traverse the tree and initiate or complete the work:
有四個主要的函數被用到遍歷樹和初始化或者完成工做單元:
來看看下面的動畫來演示在遍歷fiber樹時這些函數是怎麼工做的。我簡化了這些函數在這個demo中的運行過程。每一個函數都在一個fiber節點運行,當React你能夠看見如今活動中fiber節點的改變。你能夠清楚的看見算法是怎樣從一個分支到另外一個分支的。它首先從子元素,而後到父元素。
注意垂直的直線鏈接表示兄弟元素,橫向的鏈接表明父子,好比,b1沒有子元素,而被b2有一個子元素c1
Let’s start with the first two functions performUnitOfWork and beginWork:
讓咱們先從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; }
performUnitOfWork 函數從workInProgress 樹中接受一個fiber節點,而後經過調用beginWork 開始工做。這個函數將會啓動全部的須要fiber運行的活動。爲了達到演示的目的,咱們簡單的到引出fiber的名字來代表這個工做已經完成。beginWork 老是返回指向下一個字元素的指針或者null。
若是有下一個子元素,在workLoop函數中賦值給nextUnitOfWork這個變量。若果沒有子元素,React就知道到達了這個分支的底部元素,那麼就能夠完成如今這個節點。一旦當前這個節點結束時,就會開始兄弟元素或回到的父元素的工做單元。這項工做在 completeUnitOfWork 函數中完成:
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 sibling return 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; }
You can see that the gist of the function is a big while loop. React gets into this function when a workInProgress node has no children. After completing the work for the current fiber, it checks if there’s a sibling. If found, React exits the function and returns the pointer to the sibling. It will be assigned to the nextUnitOfWork variable and React will perform the work for the branch starting with this sibling. It’s important to understand that at this point React has only completed work for the preceding siblings. It hasn’t completed work for the parent node. Only once all branches starting with child nodes are completed does it complete the work for the parent node and backtracks.
你能夠看見這個函數實際上是一個大的完整的循環。
這個階段由completeRoot函數開始。在這個階段React更新Dom和觸發mutation生命週期函數。當進入這個階段, React有兩個數結構和effect列表。第一個樹結構是如今渲染到屏幕上的state。另一個是在render階段用來替換的是結構。它在源代碼中被稱爲finishedWork 或者workInProgress 。
而後說道effects列表——finishedWork tree與nextEffect 指針相關聯的節點的子集。記住effects列表是render階段的成果。整個render階段的意義是決定哪些節點須要插入,更新,或者刪除,哪些組件須要調用他們的生命週期函數。這就是effect要告訴咱們的。
在commit階段運行的主要函數是commitRoot。下面的基本上就是它的工做:
-在有快照 effect的標籤的節點上調用getSnapshotBeforeUpdate 生命週期函數。
commitAllHostEffects是React進行Dom更新的函數。這個函數經過下面的操做進行Dom更新:
function commitAllHostEffects() { switch (primaryEffectTag) { case Placement: { commitPlacement(nextEffect); ... } case PlacementAndUpdate: { commitPlacement(nextEffect); commitWork(current, nextEffect); ... } case Update: { commitWork(current, nextEffect); ... } case Deletion: { commitDeletion(nextEffect); ... } } }
commitAllLifecycles 是調用componentDidUpdate 和componentDidMount生命週期函數的方法。
asdasd