前面的文章分析了 Concurrent 模式下異步更新的邏輯,以及 Fiber 架構是如何進行時間分片的,更新過程當中的不少內容都省略了,評論區也收到了一些同窗對更新過程的疑惑,今天的文章就來說解下 React Fiber 架構的更新機制。react
咱們先回顧一下 Fiber 節點的數據結構(以前文章省略了一部分屬性,因此和以前文章略有不一樣):git
function FiberNode (tag, key) { // 節點 key,主要用於了優化列表 diff this.key = key // 節點類型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ... this.tag = tag // 子節點 this.child = null // 父節點 this.return = null // 兄弟節點 this.sibling = null // 更新隊列,用於暫存 setState 的值 this.updateQueue = null // 新傳入的 props this.pendingProps = pendingProps; // 以前的 props this.memoizedProps = null; // 以前的 state this.memoizedState = null; // 節點更新過時時間,用於時間分片 // react 17 改成:lanes、childLanes this.expirationTime = NoLanes this.childExpirationTime = NoLanes // 對應到頁面的真實 DOM 節點 this.stateNode = null // Fiber 節點的副本,能夠理解爲備胎,主要用於提高更新的性能 this.alternate = null // 反作用相關,用於標記節點是否須要更新 // 以及更新的類型:替換成新節點、更新屬性、更新文本、刪除…… this.effectTag = NoEffect // 指向下一個須要更新的節點 this.nextEffect = null this.firstEffect = null this.lastEffect = null }
能夠注意到 Fiber 節點有個 alternate
屬性,該屬性在節點初始化的時候默認爲空(this.alternate = null
)。這個節點的做用就是用來緩存以前的 Fiber 節點,更新的時候會判斷 fiber.alternate
是否爲空來肯定當前是首次渲染仍是更新。下面咱們上代碼:github
import React from 'react'; import ReactDOM from 'react-dom'; class App extends React.Component { state = { val: 0 } render() { return <div>val: { this.state.val }</div> } } ReactDOM.unstable_createRoot( document.getElementById('root') ).render(<App />)
在調用 createRoot 的時候,會先生成一個FiberRootNode
,在 FiberRootNode
下會有個 current 屬性,current 指向 RootFiber
能夠理解爲一個空 Fiber。後續調用的 render 方法,就是將傳入的組件掛載到 FiberRootNode.current
(即 RootFiber
) 的空 Fiber 節點上。數組
// 實驗版本對外暴露的 createRoot 須要加上 `unstable_` 前綴 exports.unstable_createRoot = createRoot function createRoot(container) { return new ReactDOMRoot(container) } function ReactDOMRoot(container) { var root = new FiberRootNode() // createRootFiber => createFiber => return new FiberNode(tag); root.current = createRootFiber() // 掛載一個空的 fiber 節點 this._internalRoot = root } ReactDOMRoot.prototype.render = function render(children) { var root = this._internalRoot var update = createUpdate() update.payload = { element: children } const rootFiber = root.current // update對象放到 rootFiber 的 updateQueue 中 enqueueUpdate(rootFiber, update) // 開始更新流程 scheduleUpdateOnFiber(rootFiber) }
render
最後調用 scheduleUpdateOnFiber
進入更新任務,該方法以前有說明,最後會經過 scheduleCallback 走 MessageChannel 消息進入下個任務隊列,最後調用 performConcurrentWorkOnRoot
方法。緩存
// scheduleUpdateOnFiber // => ensureRootIsScheduled // => scheduleCallback(performConcurrentWorkOnRoot) function performConcurrentWorkOnRoot(root) { renderRootConcurrent(root) } function renderRootConcurrent(root) { // workInProgressRoot 爲空,則建立 workInProgress if (workInProgressRoot !== root) { createWorkInProgress() } } function createWorkInProgress() { workInProgressRoot = root var current = root.current var workInProgress = current.alternate; if (workInProgress === null) { // 第一次構建,須要建立副本 workInProgress = createFiber(current.tag) workInProgress.alternate = current current.alternate = workInProgress } else { // 更新過程能夠複用 workInProgress.nextEffect = null workInProgress.firstEffect = null workInProgress.lastEffect = null } }
開始更新時,若是 workInProgress
爲空會指向一個新的空 Fiber 節點,表示正在進行工做的 Fiber 節點。數據結構
workInProgress.alternate = current current.alternate = workInProgress
構造好 workInProgress
以後,就會開始在新的 RootFiber 下生成新的子 Fiber 節點了。架構
function renderRootConcurrent(root) { // 構造 workInProgress... // workInProgress.alternate = current // current.alternate = workInProgress // 進入遍歷 fiber 樹的流程 workLoopConcurrent() } function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork() } } function performUnitOfWork() { var current = workInProgress.alternate // 返回當前 Fiber 的 child const next = beginWork(current, workInProgress) // 省略後續代碼... }
按照咱們前面的案例, workLoopConcurrent
調用完成後,最後獲得的 fiber 樹以下:app
class App extends React.Component { state = { val: 0 } render() { return <div>val: { this.state.val }</div> } }
最後進入 Commit 階段的時候,會切換 FiberRootNode 的 current 屬性:dom
function performConcurrentWorkOnRoot() { renderRootConcurrent() // 結束遍歷流程,fiber tree 已經構造完畢 var finishedWork = root.current.alternate root.finishedWork = finishedWork commitRoot(root) } function commitRoot() { var finishedWork = root.finishedWork root.finishedWork = null root.current = finishedWork // 切換到新的 fiber 樹 }
上面的流程爲第一次渲染,經過 setState({ val: 1 })
更新時,workInProgress
會切換到 root.current.alternate
。異步
function createWorkInProgress() { workInProgressRoot = root var current = root.current var workInProgress = current.alternate; if (workInProgress === null) { // 第一次構建,須要建立副本 workInProgress = createFiber(current.tag) workInProgress.alternate = current current.alternate = workInProgress } else { // 更新過程能夠複用 workInProgress.nextEffect = null workInProgress.firstEffect = null workInProgress.lastEffect = null } }
在後續的遍歷過程當中(workLoopConcurrent()
),會在舊的 RootFiber 下構建一個新的 fiber tree,而且每一個 fiber 節點的 alternate 都會指向 current fiber tree 下的節點。
這樣 FiberRootNode 的 current 屬性就會輪流在兩棵 fiber tree 不停的切換,即達到了緩存的目的,也不會過度的佔用內存。
在 React 15 裏,屢次 setState 會被放到一個隊列中,等待一次更新。
// setState 方法掛載到原型鏈上 ReactComponent.prototype.setState = function (partialState, callback) { // 調用 setState 後,會調用內部的 updater.enqueueSetState this.updater.enqueueSetState(this, partialState) }; var ReactUpdateQueue = { enqueueSetState(component, partialState) { // 在組件的 _pendingStateQueue 上暫存新的 state if (!component._pendingStateQueue) { component._pendingStateQueue = [] } // 將 setState 的值放入隊列中 var queue = component._pendingStateQueue queue.push(partialState) enqueueUpdate(component) } }
一樣在 Fiber 架構中,也會有一個隊列用來存放 setState 的值。每一個 Fiber 節點都有一個 updateQueue
屬性,這個屬性就是用來緩存 setState 值的,只是結構從 React 15 的數組變成了鏈表結構。
不管是首次 Render 的 Mount 階段,仍是 setState 的 Update 階段,內部都會調用 enqueueUpdate
方法。
// --- Render 階段 --- function initializeUpdateQueue(fiber) { var queue = { baseState: fiber.memoizedState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null }, effects: null } fiber.updateQueue = queue } ReactDOMRoot.prototype.render = function render(children) { var root = this._internalRoot var update = createUpdate() update.payload = { element: children } const rootFiber = root.current // 初始化 rootFiber 的 updateQueue initializeUpdateQueue(rootFiber) // update 對象放到 rootFiber 的 updateQueue 中 enqueueUpdate(rootFiber, update) // 開始更新流程 scheduleUpdateOnFiber(rootFiber) } // --- Update 階段 --- Component.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState) } var classComponentUpdater = { enqueueSetState: function (inst, payload) { // 獲取實例對應的fiber var fiber = get(inst) var update = createUpdate() update.payload = payload // update 對象放到 rootFiber 的 updateQueue 中 enqueueUpdate(fiber, update) scheduleUpdateOnFiber(fiber) } }
enqueueUpdate
方法的主要做用就是將 setState 的值掛載到 Fiber 節點上。
function enqueueUpdate(fiber, update) { var updateQueue = fiber.updateQueue; if (updateQueue === null) { // updateQueue 爲空則跳過 return; } var sharedQueue = updateQueue.shared; var pending = sharedQueue.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } sharedQueue.pending = update; }
屢次 setState 會在 sharedQueue.pending
上造成一個單向循環鏈表,具體例子更形象的展現下這個鏈表結構。
class App extends React.Component { state = { val: 0 } click () { for (let i = 0; i < 3; i++) { this.setState({ val: this.state.val + 1 }) } } render() { return <div onClick={() => { this.click() }}>val: { this.state.val }</div> } }
點擊 div 以後,會連續進行三次 setState,每次 setState 都會更新 updateQueue。
更新過程當中,咱們遍歷下 updateQueue 鏈表,能夠看到結果與預期的一致。
let $pending = sharedQueue.pending // 遍歷鏈表,在控制檯輸出 payload while($pending) { console.log('update.payload', $pending.payload) $pending = $pending.next }
Fiber 架構下每一個節點都會經歷遞(beginWork)
和歸(completeWork)
兩個過程:
先回顧下這個流程:
function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork() } } function performUnitOfWork() { var current = workInProgress.alternate // 返回當前 Fiber 的 child const next = beginWork(current, workInProgress) if (next === null) { // child 不存在 completeUnitOfWork() } else { // child 存在 // 重置 workInProgress 爲 child workInProgress = next } } function completeUnitOfWork() { // 向上回溯節點 let completedWork = workInProgress while (completedWork !== null) { // 收集反作用,主要是用於標記節點是否須要操做 DOM var current = completedWork.alternate completeWork(current, completedWork) // 省略構造 Effect List 過程 // 獲取 Fiber.sibling let siblingFiber = workInProgress.sibling if (siblingFiber) { // sibling 存在,則跳出 complete 流程,繼續 beginWork workInProgress = siblingFiber return } completedWork = completedWork.return workInProgress = completedWork } }
先看看 beginWork
進行了哪些操做:
function beginWork(current, workInProgress) { if (current !== null) { // current 不爲空,表示須要進行 update var oldProps = current.memoizedProps // 原先傳入的 props var newProps = workInProgress.pendingProps // 更新過程當中新的 props // 組件的 props 發生變化,或者 type 發生變化 if (oldProps !== newProps || workInProgress.type !== current.type) { // 設置更新標誌位爲 true didReceiveUpdate = true } } else { // current 爲空表示首次加載,須要進行 mount didReceiveUpdate = false } // tag 表示組件類型,不用類型的組件調用不一樣方法獲取 child switch(workInProgress.tag) { // 函數組件 case FunctionComponent: return updateFunctionComponent(current, workInProgress, newProps) // Class組件 case ClassComponent: return updateClassComponent(current, workInProgress, newProps) // DOM 原生組件(div、span、button……) case HostComponent: return updateHostComponent(current, workInProgress) // DOM 文本組件 case HostText: return updateHostText(current, workInProgress) } }
首先判斷 current(即:workInProgress.alternate)
是否存在,若是存在表示須要更新,不存在就是首次加載,didReceiveUpdate
變量設置爲 false,didReceiveUpdate
變量用於標記是否須要調用 render 新建 fiber.child
,若是爲 false 就會從新構建fiber.child
,不然複用以前的 fiber.child
。
而後會依據 workInProgress.tag
調用不一樣的方法構建 fiber.child
。關於 workInProgress.tag
的含義能夠參考 react/packages/shared/ReactWorkTags.js,主要是用來區分每一個節點各自的類型,下面是經常使用的幾個:
var FunctionComponent = 0; // 函數組件 var ClassComponent = 1; // Class組件 var HostComponent = 5; // 原生組件 var HostText = 6; // 文本組件
調用的方法不一一展開講解,咱們只看看 updateClassComponent
:
// 更新 class 組件 function updateClassComponent(current, workInProgress, newProps) { // 更新 state,省略了一萬行代碼,只保留了核心邏輯,看看就好 var oldState = workInProgress.memoizedState var newState = oldState var queue = workInProgress.updateQueue var pendingQueue = queue.shared.pending var firstUpdate = pendingQueue var update = pendingQueue do { // 合併 state var partialState = update.payload newState = Object.assign({}, newState, partialState) // 鏈表遍歷完畢 update = update.next if (update === firstUpdate) { // 鏈表遍歷完畢 queue.shared.pending = null break } } while (true) workInProgress.memoizedState = newState // state 更新完畢 // 檢測 oldState 和 newState 是否一致,若是一致,跳過更新 // 調用 componentWillUpdate 判斷是否須要更新 var instance = workInProgress.stateNode instance.props = newProps instance.state = newState // 調用 Component 實例的 render var nextChildren = instance.render() reconcileChildren(current, workInProgress, nextChildren) return workInProgress.child }
首先遍歷了以前提到的 updateQueue
更新 state
,而後就是判斷 state
是否更新,以此來推到組件是否須要更新(這部分代碼省略了),最後調用的組件 render
方法生成子組件的虛擬 DOM。最後的 reconcileChildren
就是依據 render
的返回值來生成 fiber 節點並掛載到 workInProgress.child
上。
// 構造子節點 function reconcileChildren(current, workInProgress, nextChildren) { if (current === null) { workInProgress.child = mountChildFibers( workInProgress, null, nextChildren ) } else { workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren ) } } // 兩個方法本質上同樣,只是一個須要生成新的 fiber,一個複用以前的 var reconcileChildFibers = ChildReconciler(true) var mountChildFibers = ChildReconciler(false) function ChildReconciler(shouldTrackSideEffects) { return function (returnFiber, currentChild, nextChildren) { // 不一樣類型進行不一樣的處理 // 返回對象 if (typeof newChild === 'object' && newChild !== null) { return placeSingleChild( reconcileSingleElement( returnFiber, currentChild, newChild ) ) } // 返回數組 if (Array.isArray(newChild)) { // ... } // 返回字符串或數字,代表是文本節點 if ( typeof newChild === 'string' || typeof newChild === 'number' ) { // ... } // 返回 null,直接刪除節點 return deleteRemainingChildren(returnFiber, currentChild) } }
篇幅有限,看看 render 返回值爲對象的狀況(一般狀況下,render 方法 return 的若是是 jsx 都會被轉化爲虛擬 DOM,而虛擬 DOM 一定是對象或數組):
if (typeof newChild === 'object' && newChild !== null) { return placeSingleChild( // 構造 fiber,或者是複用 fiber reconcileSingleElement( returnFiber, currentChild, newChild ) ) } function placeSingleChild(newFiber) { // 更新操做,須要設置 effectTag if (shouldTrackSideEffects && newFiber.alternate === null) { newFiber.effectTag = Placement } return newFiber }
當 fiber.child
爲空時,就會進入 completeWork
流程。而 completeWork
主要就是收集 beginWork
階段設置的 effectTag
,若是有設置 effectTag
就代表該節點發生了變動, effectTag
的主要類型以下(默認爲 NoEffect
,表示節點無需進行操做,完整的定義能夠參考 react/packages/shared/ReactSideEffectTags.js):
export const NoEffect = /* */ 0b000000000000000; export const PerformedWork = /* */ 0b000000000000001; // You can change the rest (and add more). export const Placement = /* */ 0b000000000000010; export const Update = /* */ 0b000000000000100; export const PlacementAndUpdate = /* */ 0b000000000000110; export const Deletion = /* */ 0b000000000001000; export const ContentReset = /* */ 0b000000000010000; export const Callback = /* */ 0b000000000100000; export const DidCapture = /* */ 0b000000001000000;
咱們看看 completeWork
過程當中,具體進行了哪些操做:
function completeWork(current, workInProgress) { switch (workInProgress.tag) { // 這些組件沒有反應到 DOM 的 effect,跳過處理 case Fragment: case MemoComponent: case LazyComponent: case ContextConsumer: case FunctionComponent: return null // class 組件 case ClassComponent: { // 處理 context var Component = workInProgress.type if (isContextProvider(Component)) { popContext(workInProgress) } return null } case HostComponent: { // 這裏 Fiber 的 props 對應的就是 DOM 節點的 props // 例如: id、src、className …… var newProps = workInProgress.pendingProps // props if ( current !== null && workInProgress.stateNode != null ) { // current 不爲空,表示是更新操做 var type = workInProgress.type updateHostComponent(current, workInProgress, type, newProps) } else { // current 爲空,表示須要渲染 DOM 節點 // 實例化 DOM,掛載到 fiber.stateNode var instance = createInstance(type, newProps) appendAllChildren(instance, workInProgress, false, false); workInProgress.stateNode = instance } return null } case HostText: { var newText = workInProgress.pendingProps // props if (current && workInProgress.stateNode != null) { var oldText = current.memoizedProps // 更新文本節點 updateHostText(current, workInProgress, oldText, newText) } else { // 實例文本節點 workInProgress.stateNode = createTextInstance(newText) } return null } } }
與 beginWork
同樣,completeWork
過程當中也會依據 workInProgress.tag
來進行不一樣的處理,其餘類型的組件基本能夠略過,只用關注下 HostComponent
、HostText
,這兩種類型的節點會反應到真實 DOM 中,因此會有所處理。
updateHostComponent = function ( current, workInProgress, type, newProps ) { var oldProps = current.memoizedProps if (oldProps === newProps) { // 新舊 props 無變化 return } var instance = workInProgress.stateNode // DOM 實例 // 對比新舊 props var updatePayload = diffProperties(instance, type, oldProps, newProps) // 將發生變化的屬性放入 updateQueue // 注意這裏的 updateQueue 不一樣於 Class 組件對應的 fiber.updateQueue workInProgress.updateQueue = updatePayload };
updateHostComponent
方法最後會經過 diffProperties
方法獲取一個更新隊列,掛載到 fiber.updateQueue
上,這裏的 updateQueue 不一樣於 Class 組件對應的 fiber.updateQueue
,不是一個鏈表結構,而是一個數組結構,用於更新真實 DOM。
下面舉一個例子,修改 App 組件的 state 後,下面的 span 標籤對應的 data-val
、style
、children
都會相應的發生修改,同時,在控制檯打印出 updatePayload
的結果。
import React from 'react' class App extends React.Component { state = { val: 1 } clickBtn = () => { this.setState({ val: this.state.val + 1 }) } render() { return (<div> <button onClick={this.clickBtn}>add</button> <span data-val={this.state.val} style={{ fontSize: this.state.val * 15 }} > { this.state.val } </span> </div>) } } export default App
在最後的更新階段,爲了避免用遍歷全部的節點,在 completeWork
過程結束後,會構造一個 effectList 鏈接全部 effectTag 不爲 NoEffect 的節點,在 commit 階段可以更高效的遍歷節點。
function completeUnitOfWork() { let completedWork = workInProgress while (completedWork !== null) { // 調用 completeWork()... // 構造 Effect List 過程 var returnFiber = completedWork.return if (returnFiber !== null) { if (returnFiber.firstEffect === null) { returnFiber.firstEffect = completedWork.firstEffect; } if (completedWork.lastEffect !== null) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork.firstEffect; } returnFiber.lastEffect = completedWork.lastEffect; } if (completedWork.effectTag > PerformedWork) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork } else { returnFiber.firstEffect = completedWork } returnFiber.lastEffect = completedWork } } // 判斷 completedWork.sibling 是否存在... } }
上面的代碼就是構造 effectList 的過程,光看代碼仍是比較難理解的,咱們仍是經過實際的代碼來解釋一下。
import React from 'react' export default class App extends React.Component { state = { val: 0 } click = () => { this.setState({ val: this.state.val + 1 }) } render() { const { val } = this.state const array = Array(2).fill() const rows = array.map( (_, row) => <tr key={row}> {array.map( (_, col) => <td key={col}>{val}</td> )} </tr> ) return <table onClick={() => this.click()}> {rows} </table> } }
咱們構造一個 2 * 2 的 Table,每次點擊組件,td 的 children 都會發生修改,下面看看這個過程當中的 effectList 是如何變化的。
第一個 td 完成 completeWork
後,EffectList 結果以下:
第二個 td 完成 completeWork
後,EffectList 結果以下:
兩個 td 結束了 completeWork
流程,會回溯到 tr 進行 completeWork
,tr 結束流程後 ,table 會直接複用 tr 的 firstEffect 和 lastEffect,EffectList 結果以下:
後面兩個 td 結束 completeWork
流程後,EffectList 結果以下:
回溯到第二個 tr 進行 completeWork
,因爲 table 已經存在 firstEffect 和 lastEffect,這裏會直接修改 table 的 firstEffect 的 nextEffect,以及從新指定 lastEffect,EffectList 結果以下:
最後回溯到 App 組件時,就會直接複用 table 的 firstEffect 和 lastEffect,最後 的EffectList 結果以下:
這一階段的主要做用就是遍歷 effectList 裏面的節點,將更新反應到真實 DOM 中,固然還涉及一些生命週期鉤子的調用,咱們這裏只展現最簡單的邏輯。
function commitRoot(root) { var finishedWork = root.finishedWork var firstEffect = finishedWork var nextEffect = firstEffect // 遍歷effectList while (nextEffect !== null) { const effectTag = nextEffect.effectTag // 根據 effectTag 進行不一樣的處理 switch (effectTag) { // 插入 DOM 節點 case Placement: { commitPlacement(nextEffect) nextEffect.effectTag &= ~Placement break } // 更新 DOM 節點 case Update: { const current = nextEffect.alternate commitWork(current, nextEffect) break } // 刪除 DOM 節點 case Deletion: { commitDeletion(root, nextEffect) break } } nextEffect = nextEffect.nextEffect } }
這裏再也不展開講解每一個 effect 下具體的操做,在遍歷完 effectList 以後,就是將當前的 fiber 樹進行切換。
function commitRoot() { var finishedWork = root.finishedWork // 遍歷 effectList …… root.finishedWork = null root.current = finishedWork // 切換到新的 fiber 樹 }
到這裏整個更新流程就結束了,能夠看到 Fiber 架構下,全部數據結構都是鏈表形式,鏈表的遍歷都是經過循環的方式來實現的,看代碼的過程當中常常會被忽然出現的 return、break 擾亂思路,因此要徹底理解這個流程仍是很不容易的。
最後,但願你們在閱讀文章的過程當中能有收穫,下一篇文章會開始寫 Hooks 相關的內容。