How React Works (一)首次渲染


How React Works (一)首次渲染

一、前言

  本文將會通過一個簡單的例子,結合React源碼(v 16.4.2)來說明 React 是如何工作的,並且幫助讀者理解 ReactElement、Fiber 之間的關系,以及 Fiber 在各個流程的作用。看完這篇文章有助於幫助你更加容易地讀懂 React 源碼。初期計划有以下幾篇文章:

  1. 首次渲染
  2. 事件機制
  3. 更新流程
  4. 調度機制

二、核心類型解析

  
  在正式進入流程講解之前,先了解一下 React 源碼內部的核心類型,有助於幫助我們更好地了解整個流程。為了讓大家更加容易理解,后續的描述只抽取核心部分,把 ref、context、異步、調度、異常處理 之類的簡化掉了。
  

1. ReactElement

  我們寫 React 組件的時候,通常會使用JSX來描述組件。<p></p>這種寫法經過babel轉換后,會變成以 React.createElement(type, props, children)形式。而我們的例子中,type會是兩種類型:functionstring,實際上就是Appconstructor方法,以及其他HTML標簽。

  而這個方法,最終是會返回一個 ReactElement ,他是一個普通的 Object ,不是通過某個 class 實例化二來的,大概看看即可,核心成員如下:

key type desc
$$typeof Symbol|Number 對象類型標識,用於判斷當前Object是否一個某種類型的ReactElement
type Function|String|Symbol|Number|Object 如果當前ReactElement是是一個ReactComponent,那這里將是它對應的Constructor;而普通HTML標簽,一般都是String
props Object ReactElement上的所有屬性,包含children這個特殊屬性

2. ReactRoot

  當前放在ReactDom.js內部,可以理解為React渲染的入口。我們調用ReactDom.render之后,核心就是創建一個 ReactRoot ,然后調用 ReactRoot 實例的render方法,進入渲染流程的。

key type desc
render Function 渲染入口方法
_internalRoot FiberRoot 根據當前DomContainer創建的一個FiberTree的根

3. FiberRoot

  FiberRoot 是一個 Object ,是后續初始化、更新的核心根對象。核心成員如下:

key type desc
current (HostRoot)FiberNode 指向當前已經完成的Fiber Tree 的Root
containerInfo DomContainer 根據當前DomContainer創建的一個FiberTree的根
finishedWork (HostRoot)FiberNode|null 指向當前已經完成准備工作的Fiber Tree Root

current、finishedWork,都是一個(HostRoot)FiberNode,到底是為什么呢?先賣個關子,后面將會講解。

4. FiberNode

  在 React 16之后,Fiber Reconciler 就作為 React 的默認調度器,核心數據結構就是由FiberNode組成的 Node Tree 。先參觀下他的核心成員:

key type desc
實例相關 --- ---
tag Number FiberNode的類型,可以在packages/shared/ReactTypeOfWork.js中找到。當前文章 demo 可以看到ClassComponent、HostRoot、HostComponent、HostText這幾種
type Function|String|Symbol|Number|Object 和ReactElement表現一致
stateNode FiberRoot|DomElement|ReactComponentInstance FiberNode會通過stateNode綁定一些其他的對象,例如FiberNode對應的Dom、FiberRoot、ReactComponent實例
Fiber遍歷流程相關
return FiberNode|null 表示父級 FiberNode
child FiberNode|null 表示第一個子 FiberNode
sibling FiberNode|null 表示緊緊相鄰的下一個兄弟 FiberNode
alternate FiberNode|null Fiber調度算法采取了雙緩沖池算法,FiberRoot底下的所有節點,都會在算法過程中,嘗試創建自己的“鏡像”,后面將會繼續講解
數據相關
pendingProps Object 表示新的props
memoizedProps Object 表示經過所有流程處理后的新props
memoizedState Object 表示經過所有流程處理后的新state
副作用描述相關
updateQueue UpdateQueue 更新隊列,隊列內放着即將要發生的變更狀態,詳細內容后面再講解
effectTag Number 16進制的數字,可以理解為通過一個字段標識n個動作,如Placement、Update、Deletion、Callback……所以源碼中看到很多 &=
firstEffect FiberNode|null 與副作用操作遍歷流程相關 當前節點下,第一個需要處理的副作用FiberNode的引用
nextEffect FiberNode|null 表示下一個將要處理的副作用FiberNode的引用
lastEffect FiberNode|null 表示最后一個將要處理的副作用FiberNode的引用

5. Update

  在調度算法執行過程中,會將需要進行變更的動作以一個Update數據來表示。同一個隊列中的Update,會通過next屬性串聯起來,實際上也就是一個單鏈表。

key type desc
tag Number 當前有0~3,分別是UpdateState、ReplaceState、ForceUpdate、CaptureUpdate
payload Function|Object 表示這個更新對應的數據內容
callback Function 表示更新后的回調函數,如果這個回調有值,就會在UpdateQueue的副作用鏈表中掛在當前Update對象
next Update UpdateQueue中的Update之間通過next來串聯,表示下一個Update對象

6. UpdateQueue

  在 FiberNode 節點中表示當前節點更新、更新的副作用(主要是Callback)的集合,下面的結構省略了CapturedUpdate部分

key type desc
baseState Object 表示更新前的基礎狀態
firstUpdate Update 第一個 Update 對象引用,總體是一條單鏈表
lastUpdate Update 最后一個 Update 對象引用
firstEffect Update 第一個包含副作用(Callback)的 Update 對象的引用
lastEffect Update 最后一個包含副作用(Callback)的 Update 對象的引用

三、代碼樣例

  本次流程說明,使用下面的源碼進行分析


//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));



//App.js
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      msg:'init',
    };
  }
  render() {
    return (
      <div className="App">
        <p className="App-intro">
          To get started, edit <code>{this.state.msg}</code> and save to reload.
        </p>
        <button onClick={() => {
          this.setState({msg: 'clicked'});
        }}>hehe
        </button>
      </div>
    );
  }

}

export default App;

四、渲染調度算法 - 准備階段

  從ReactDom.render方法開始,正式進入渲染的准備階段。

1. 初始化基本節點

  創建 ReactRoot、FiberRoot、(HostRoot)FiberNode,建立他們與 DomContainer 的關系。

2. 初始化(HostRoot)FiberNodeUpdateQueue

  通過調用ReactRoot.render,然后進入packages/react-reconciler/src/ReactFiberReconciler.jsupdateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate一系列方法調用,為這次初始化創建一個Update,把<App />這個 ReactElement 作為 Update 的payload.element的值,然后把 Update 放到 (HostRoot)FiberNode 的 updateQueue 中。

然后調用scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot,期間主要是提取當前應該進行初始化的 (HostFiber)FiberNode,后續正式進入算法執行階段。

五、渲染調度算法 - 執行階段

  由於本次是初始化,所以需要調用packages/react-reconciler/src/ReactFiberScheduler.jsrenderRoot方法,生成一棵完整的FiberNode Tree finishedWork

1. 生成 (HostRoot)FiberNode 的workInProgress,即current.alternate

  在整個算法過程中,主要做的事情是遍歷 FiberNode 節點。算法中有兩個角色,一是表示當前節點原始形態的current節點,另一個是表示基於當前節點進行重新計算的workInProgress/alternate節點。兩個對象實例是獨立的,相互之前通過alternate屬性相互引用。對象的很多屬性都是先復制再重建的。

第一次創建結果示意圖:

  這個做法的核心思想是雙緩池技術(double buffering pooling technique),因為需要做 diff 的話,起碼是要有兩棵樹進行對比。通過這種方式,可以把樹的總體數量限制在2,節點、節點屬性都是延遲創建的,最大限度地避免內存使用量因算法過程而不斷增長。后面的更新流程的文章里,會了解到這個雙緩沖怎么玩。

2. 工作執行循環

示意代碼如下:

nextUnitOfWork = createWorkInProgress(
  nextRoot.current,
  null,
  nextRenderExpirationTime,
);
....

while (nextUnitOfWork !== null) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

剛剛創建的 FiberNode 被作為nextUnitOfWork,從此進入工作循環。從上面的代碼可以看出,在是一個典型的遞歸的循環寫法。這樣寫成循環,一來就是和傳統的遞歸改循環寫法一樣,避免調用棧不斷堆疊以及調用棧溢出等問題;二來在結合其他Scheduler代碼的輔助變量,可以實現遍歷隨時終止、隨時恢復的效果。

我們繼續深入performUnitOfWork函數,可以看到類似的代碼框架:

const current = workInProgress.alternate;
//...
next = beginWork(current, workInProgress, nextRenderExpirationTime);
//...
if (next === null) {
    next = completeUnitOfWork(workInProgress);
}
//...
return next;

從這里可以看出,這里對 workInProgress 節點進行一些處理,然后會通過一定的遍歷規則返回next,如果next不為空,就返回進入下一個performUnitOfWork,否則就進入completeUnitOfWork

3. beginWork

  每個工作的對象主要是處理workInProgress。這里通過workInProgress.tag區分出當前 FiberNode 的類型,然后進行對應的更新處理。下面介紹我們例子里面可以遇到的兩種處理比較復雜的 FiberNode 類型的處理過程,然后再單獨講解里面比較重要的processUpdateQueue以及reconcileChildren過程。

3.1 HostRoot - updateHostRoot

  HostRoot,即文中經常講到的 (HostRoot)FiberNode,表示它是一個 HostRoot 類型的 FiberNode ,代碼中通過FiberRoot.tag表示。

  前面講到,在最開始初始化的時候,(HostRoot)FiberNode 在初始化之后,初始化了他的updateQueue,里面放了准備處理的子節點。這里就做兩個動作:

  • 處理更新隊列,得出新的state - processUpdateQueue方法
  • 創建或者更新 FiberNode 的child,得到下一個工作循環的入參(也是FiberNode) - ChildReconciler方法

  通過這兩個函數的詳細內容屬於比較通用的部分,將在后面單獨講解。

3.2 ClassComponent - updateClassComponent

  ClassComponent,即我們在寫 React 代碼的時候自己寫的 Component,即例子中的App

3.2.1 創建ReactComponent實例階段

  對於尚未初始化的節點,這個方法主要是通過FiberNode.type這個 ReactComponent Constructor 來創建 ReactComponent 實例並創建與 FiberNode 的關系。

(ClassComponent)FiberNode 與 ReactComponent 的關系示意圖:

  初始化后,會進入實例的mount過程,即把 Component render之前的周期方法都調用完。期間,state可能會被以下流程修改:

  • 調用getDerivedStateFromProps
  • 調用componentWillMount -- deprecated
  • 處理因上面的流程產生的Update所調用的processUpdateQueue
3.2.2 完成階段 - 創建 child FiberNode

  在上面初始化Component實例之后,通過調用實例的render獲取子 ReactElement,然后創建對應的所有子 FiberNode 。最終將workInProgress.child指向第一個子 FiberNode。

3.4 處理節點的更新隊列 - processUpdateQueue 方法

  在解釋流程之前,先回顧一下updateQueue的數據結構:

  從上面的結構可以看出,UpdateQueue 是存放整個 Update 單向鏈表的容器。里面的 baseState 表示更新前的原始 State,而通過遍歷各個 Update 鏈表后,最終會得到一個新的 baseState。

  對於單個 Update 的處理,主要是根據Update.tag來進行區分處理。

  • ReplaceState:直接返回這里的 payload。如果 payload 是函數,則使用它的返回值作為新的 State。
  • CaptureUpdate:僅僅是將workInProgress.effectTag設置為清空ShouldCapture標記位,增加DidCapture標記位。
  • UpdateState:如果payload是普通對象,則把他當做新 State。如果 payload 是函數,則把執行函數得到的返回值作為新 State。如果新 State 不為空,則與原來的 State 進行合並,返回一個新對象
  • ForceUpdate:僅僅是設置 hasForceUpdate為 true,返回原始的 State。

  整體而言,這個方法要做的事情,就是遍歷這個 UpdateQueue ,然后計算出最后的新 State,然后存到workInProgress.memoizedState中。

3.5 處理子FiberNode - reconcileChildren 方法

  在 workInProgress 節點自身處理完成之后,會通過props.children或者instance.render方法獲取子 ReactElement。子 ReactElement 可能是對象數組字符串迭代器,針對不同的類型進行處理。

  • 下面通過 ClassComponent 及其 數組類型 child的場景來講解子 FiberNode 的創建、關聯流程(reconcileChildrenArray方法):

  在頁面初始化階段,由於沒有老節點的存在,流程上就略過了位置索引比對、兄弟元素清理等邏輯,所以這個流程相對簡單。

  遍歷之前render方法生成的 ReactElement 數組,一一對應地生成 FiberNode。FiberNode 有returnFiber屬性和sibling屬性,分別指向其父親 FiberNode和緊鄰的下一個兄弟 FiberNode。這個數據結構和后續的遍歷過程相關。

  現在,生成的FiberNode Tree 結構如下:

  圖中的兩個(HostComponent)FiberNode就是剛剛生成的子 FiberNode,即源碼中的<p>...</p><button>...</button>。這個方法最后返回的,是第一個子 FiberNode,就通過這種方式創建了(ClassComponent)FiberNode.child與第一個子 FiberNode的關系。

  這個時候,再搬出剛剛曾經看過的代碼:

const current = workInProgress.alternate;
//...
next = beginWork(current, workInProgress, nextRenderExpirationTime);
//...
if (next === null) {
    next = completeUnitOfWork(workInProgress);
}
//...
return next;

  意味着剛剛返回的 child 會被當做 next 進入下一個工作循環。如此往復,會得到下面這樣的 FiberNode Tree :

  生成這棵樹之后,被返回的是左下角的那個 (HostText)FiberNode。而重新進入beginWork方法后,由於這個 FiberNode 並沒有 child ,根據上面的代碼邏輯,會進入completeUnitOfWork方法。

注意:雖然說本例子的 FiberNode Tree 最終形態是這樣子的,但實際上算法是優先深度遍歷,到葉子節點之后再遍歷緊鄰的兄弟節點。如果兄弟節點有子節點,則會繼續擴展下去。

4. completeUnitOfWork

  進入這個流程,表明 workInProgress 節點是一個葉子節點,或者它的子節點都已經處理完成了。現在開始要完成這個節點處理的剩余工作。
  

4.1 創建DomElement,處理子DomElement 綁定關系

  completeWork方法中,會根據workInProgress.tag來區分出不同的動作,下面挑選2個比較重要的來進一步分析:

4.1.1 HostText

  此前提到過,FiberNode.stateNode可以用於存放 DomElement Instance。在初始化過程中,stateNode 為 null,所以會通過document.createTextNode創建一個 Text DomElement,節點內容就是workInProgress.memoizedProps。最后,通過__reactInternalInstance$[randomKey]屬性建立與自己的 FiberNode的聯系。

4.1.2 HostComponent

  在本例子中,處理完上面的 HostText 之后,調度算法會尋找當前節點的 sibling 節點進行處理,所以進入了HostComponent的處理流程。

  由於當前出於初始化流程,所以處理比較簡單,只是根據FiberNode.tag(當前值是code)來創建一個 DomElement,即通過document.createElement來創建節點。然后通過__reactInternalInstance$[randomKey]屬性建立與自己的 FiberNode的聯系;通過__reactEventHandlers$[randomKey]來建立與 props 的聯系。

  完成 DomElement 自身的創建之后,如果有子節點,則會將子節點 append 到當前節點中。現在先略過這個步驟。

  后續,通過setInitialProperties方法對 DomElement 的屬性進行初始化,而<code>節點的內容、樣式、class、事件 Handler等等也是這個時候存放進去的。

  現在,整個 FiberNode Tree 如下:

  經過多次循環處理,得出以下的 FiberNode Tree:

  之后,回到紅色箭頭指向的 (HostComponent)FiberNode,可以分析一下之前省略掉的子節點處理流程。

  在當前 DomElement 創建完畢后,進入appendAllChildren方法把子節點 append 到當前 DomElement 。由上面的流程可以知道,可以通過 workInProgress.child -> workInProgress.child.sibling -> workInProgress.child.sibling.sibling ....找到所有子節點,而每個節點的 stateNode 就是對應的 DomElement,所以通過這種方式的遍歷,就可以把所有的 DomElement 掛載到 父 DomElement中。

  最終,和 DomElement 相關的 FiberNode 都被處理完,得出下面的FiberNode 全貌:

4.2 將當前節點的 effect 掛在到 returnFiber 的 effect 末尾

  在前面講解基礎數據結構的時候描述過,每個 FiberNode 上都有 firstEffect、lastEffect ,指向一個Effect(副作用) FiberNode鏈表。在處理完當前節點,即將返回父節點的時候,把當前的鏈條掛接到 returnFiber 上。最終,在(HostRoot)FiberNode.firstEffect 上掛載着一條擁有當前 FiberNode Tree 所有副作用的 FiberNode 鏈表。

5. 執行階段結束

  經歷完之前的所有流程,最終 (HostRoot)FiberNode 也被處理完成,就把 (HostRoot)FiberNode 返回,最終作為finishedWork返回到 performWorkOnRoot,后續進入下一個階段。

六、渲染調度算法 - 提交階段

  所謂提交階段,就是實際執行一些周期函數、Dom 操作的階段。

  這里也是一個鏈表的遍歷,而遍歷的就是之前階段生成的 effect 鏈表。在遍歷之前,由於初始化的時候,由於 (HostRoot)FiberNode.effectTagCallback(初始化回調)),會先將 finishedWork 放到鏈表尾部。結構如下:

每個部分提交完成之后,都會把遍歷節點重置到finishedWork.firstEffect

1. 提交節點裝載( mount )前的操作

  當前這個流程處理的只有屬於 ReactComponent 的 getSnapshotBeforeUpdate方法。
  

2. 提交端原生節點( Host )的副作用(插入、修改、刪除)

  遍歷到某個節點后,會根據節點的 effectTag 決定進行什么操作,操作包括插入( Placement )修改( Update )刪除( Deletion )

  由於當前是首次渲染,所以會進入插入( Placement )流程,其余流程將在后面的《How React Works(三)更新流程》中講解。

2.1 插入流程( Placement )

  要做插入操作,必先找到兩個要素:父親 DomElement ,子 DomElement。

2.1.1 找到相對於當前 FiberNode 最近的父親 DomElement

  通過FiberNode.return不斷往上找,找到最近的(HostComponent)FiberNode、(HostRoot)FiberNode、(HostPortal)FiberNode節點,然后通過(HostComponent)FiberNode.stateNode(HostRoot)FiberNode.stateNode.containerInfo(HostPortal)FiberNode.stateNode.containerInfo就可以獲取到對應的 DomElement 實例。
  

2.1.2 找到相對於當前 FiberNode 最近的所有游離子 DomElement

  實際上,把目標是查找當前 FiberNode底下所有鄰近的 (HostComponent)FiberNode、(HostText)FiberNode,然后通過 stateNode 屬性就可以獲取到待插入的 子DomElement 。

  所謂所有鄰近的,可以通過這幅圖來理解:

  圖中紅框部分FiberNode.stateNode,就是要被添加到父親 DomElement的 子 DomElement。

  遍歷順序,和之前的生成 FiberNode Tree時順序大致相同:

a) 訪問child節點,直至找到 FiberNode.type 為 HostComponent 或者 HostRoot 的節點,獲取到對應的 stateNode ,append 到 父 DomElement中。

b) 尋找兄弟節點,如果有,就訪問兄弟節點,返回 a) 。

c) 如果沒有兄弟節點,則訪問 return 節點,如果 return 不是當前算法入參的根節點,就返回a)。

d) 如果 return 到根節點,則退出。

3. 改變 workInProgress/alternate/finishedWork 的身份

  雖然是短短的一行代碼,但這個十分重要,所以單獨標記:

    root.current = finishedWork;

  這意味着,在 DomElement 副作用處理完畢之后,意味着之前講的緩沖樹已經完成任務,翻身當主人,成為下次修改過程的current。再來看一個全貌:

4. 提交裝載、變更后的生命周期調用操作

  在這個流程中,也是遍歷 effect 鏈表,對於每種類型的節點,會做不同的處理。

4.1 ClassComponent

  如果當前節點的 effectTag 有 Update 的標志位,則需要執行對應實例的生命周期方法。在初始化階段,由於當前的 Component 是第一次渲染,所以應該執行componentDidMount,其他情況下應該執行componentDidUpdate

  之前講到,updateQueue 里面也有 effect 鏈表。里面存放的就是之前各個 Update 的 callback,通常就來源於setState的第二個參數,或者是ReactDom.rendercallback。在執行完上面的生命周期函數后,就開始遍歷這個 effect 鏈表,把 callback 都執行一次。

4.2 HostRoot

  操作和 ClassComponent 處理的第二部分一致。

4.3 HostComponent

  這部分主要是處理初次加載的 HostComponent 的獲取焦點問題,如果組件有autoFocus這個 props ,就會獲取焦點。
  
  

七、小結

  本文主要講述了ReactDom.render的內部的工作流程,描述了 React 初次渲染的內在流程:

  1. 創建基礎對象: ReactRoot、FiberRoot、(HostRoot)FiberNode
  2. 創建 HostRoot 的鏡像,通過鏡像對象來做初始化
  3. 初始化過程,通過 ReactElement 引導 FiberNode Tree 的創建
  4. 父子 FiberNode 通過childreturn連接
  5. 兄弟 FiberNode 通過sibling連接
  6. FiberNode Tree 創建過程,深度優先,到底之后創建兄弟節點
  7. 一旦到達葉子節點,就開始創建 FiberNode 對應的 實例,例如對應的 DomElement 實例、ReactComponent 實例,並將實例通過FiberNode.stateNode創建關聯。
  8. 如果當前創建的是 ReactComponent 實例,則會調用調用getDerivedStateFromPropscomponentWillMount方法
  9. DomElement 創建之后,如果 FiberNode 子節點中有創建好的 DomElement,就馬上 append 到新創建的 DomElement 中
  10. 構建完成整個FiberNode Tree 后,對應的 DomElement Tree 也創建好了,后續進入提交過程
  11. 在創建 DomElement Tree 的過程中,同時會把當前的副作用不斷往上傳遞,在提交階段里面,會找到這種標記,並把剛創建完的 DomElement Tree 裝載到容器 DomElement中
  12. 雙緩沖的兩棵樹 FiberNode Tree 角色互換,原來的 workInProgress 轉正
  13. 執行對應 ReactComponent 的裝載后生命周期方法componentDidMount
  14. 其他回調調用、autoFocus 處理

 下一篇文章將會描述 React 的事件機制(但據說准備要重構),希望我不會斷耕。

寫完第一篇,React 版本已經到了 16.5.0 ……


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM