React 架構的演變 - 更新機制
前面的文章分析了 Concurrent 模式下異步更新的邏輯,以及 Fiber 架構是如何進行時間分片的,更新過程中的很多內容都省略了,評論區(qū)也收到了一些同學對更新過程的疑惑,今天的文章就來講解下 React Fiber 架構的更新機制。
Fiber 數(shù)據(jù)結構
我們先回顧一下 Fiber 節(jié)點的數(shù)據(jù)結構(之前文章省略了一部分屬性,所以和之前文章略有不同):
function?FiberNode?(tag,?key)?{
??//?節(jié)點?key,主要用于了優(yōu)化列表?diff
??this.key?=?key
??//?節(jié)點類型;FunctionComponent:?0, ClassComponent: 1, HostRoot: 3 ...
??this.tag?=?tag
?//?子節(jié)點
??this.child?=?null
??//?父節(jié)點
??this.return?=?null?
??//?兄弟節(jié)點
??this.sibling?=?null
??
??//?更新隊列,用于暫存?setState?的值
??this.updateQueue?=?null
??//?新傳入的?props
??this.pendingProps?=?pendingProps;
??//?之前的?props
??this.memoizedProps?=?null;
??//?之前的?state
??this.memoizedState?=?null;
??//?節(jié)點更新過期時間,用于時間分片
??// react 17 改為:lanes、childLanes
??this.expirationTime?=?NoLanes
??this.childExpirationTime?=?NoLanes
??//?對應到頁面的真實?DOM?節(jié)點
??this.stateNode?=?null
??//?Fiber?節(jié)點的副本,可以理解為備胎,主要用于提升更新的性能
??this.alternate?=?null
??//?副作用相關,用于標記節(jié)點是否需要更新
??//?以及更新的類型:替換成新節(jié)點、更新屬性、更新文本、刪除……
??this.effectTag?=?NoEffect
??//?指向下一個需要更新的節(jié)點
??this.nextEffect?=?null
??this.firstEffect?=?null
??this.lastEffect?=?null
}
緩存機制
可以注意到 Fiber 節(jié)點有個 alternate 屬性,該屬性在節(jié)點初始化的時候默認為空(this.alternate = null)。這個節(jié)點的作用就是用來緩存之前的 Fiber 節(jié)點,更新的時候會判斷 fiber.alternate 是否為空來確定當前是首次渲染還是更新。下面我們上代碼:
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。后續(xù)調用的 render 方法,就是將傳入的組件掛載到 FiberRootNode.current(即 RootFiber) 的空 Fiber 節(jié)點上。
//?實驗版本對外暴露的?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?節(jié)點
??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?為空,則創(chuàng)建?workInProgress
??if?(workInProgressRoot?!==?root)?{
????createWorkInProgress()
??}
}
function?createWorkInProgress()?{
??workInProgressRoot?=?root
??var?current?=?root.current
??var?workInProgress?=?current.alternate;
??if?(workInProgress?===?null)?{
????//?第一次構建,需要創(chuàng)建副本
????workInProgress?=?createFiber(current.tag)
????workInProgress.alternate?=?current
????current.alternate?=?workInProgress
??}?else?{
????//?更新過程可以復用
????workInProgress.nextEffect?=?null
????workInProgress.firstEffect?=?null
????workInProgress.lastEffect?=?null
??}
}
開始更新時,如果 workInProgress 為空會指向一個新的空 Fiber 節(jié)點,表示正在進行工作的 Fiber 節(jié)點。
workInProgress.alternate?=?current
current.alternate?=?workInProgress
fiber tree構造好 workInProgress 之后,就會開始在新的 RootFiber 下生成新的子 Fiber 節(jié)點了。
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)
??//?省略后續(xù)代碼...
}
按照我們前面的案例, workLoopConcurrent 調用完成后,最后得到的 fiber 樹如下:
class?App?extends?React.Component?{
??state?=?{?val:?0?}
??render()?{
????return?<div>val:?{?this.state.val?}div>
??}
}
fiber tree最后進入 Commit 階段的時候,會切換 FiberRootNode 的 current 屬性:
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?樹
}
fiber tree上面的流程為第一次渲染,通過 setState({ val: 1 }) 更新時,workInProgress 會切換到 root.current.alternate。
function?createWorkInProgress()?{
??workInProgressRoot?=?root
??var?current?=?root.current
??var?workInProgress?=?current.alternate;
??if?(workInProgress?===?null)?{
????//?第一次構建,需要創(chuàng)建副本
????workInProgress?=?createFiber(current.tag)
????workInProgress.alternate?=?current
????current.alternate?=?workInProgress
??}?else?{
????//?更新過程可以復用
????workInProgress.nextEffect?=?null
????workInProgress.firstEffect?=?null
????workInProgress.lastEffect?=?null
??}
}
fiber tree在后續(xù)的遍歷過程中(workLoopConcurrent()),會在舊的 RootFiber 下構建一個新的 fiber tree,并且每個 fiber 節(jié)點的 alternate 都會指向 current fiber tree 下的節(jié)點。
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 節(jié)點都有一個 updateQueue 屬性,這個屬性就是用來緩存 setState 值的,只是結構從 React 15 的數(shù)組變成了鏈表結構。
無論是首次 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 節(jié)點上。
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 上形成一個單向循環(huán)鏈表,具體例子更形象的展示下這個鏈表結構。
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 之后,會連續(xù)進行三次 setState,每次 setState 都會更新 updateQueue。
第一次 setState
第二次 setState
第三次 setState更新過程中,我們遍歷下 updateQueue 鏈表,可以看到結果與預期的一致。
let?$pending?=?sharedQueue.pending
//?遍歷鏈表,在控制臺輸出?payload
while($pending)?{
??console.log('update.payload',?$pending.payload)
??$pending?=?$pending.next
}
鏈表數(shù)據(jù)遞歸 Fiber 節(jié)點
Fiber 架構下每個節(jié)點都會經歷遞(beginWork)和歸(completeWork)兩個過程:
- beginWork:生成新的 state,調用 render 創(chuàng)建子節(jié)點,連接當前節(jié)點與子節(jié)點;
- completeWork:依據(jù) EffectTag 收集 Effect,構造 Effect List;
先回顧下這個流程:
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()?{
??//?向上回溯節(jié)點
??let?completedWork?=?workInProgress
??while?(completedWork?!==?null)?{
????//?收集副作用,主要是用于標記節(jié)點是否需要操作?DOM
????var?current?=?completedWork.alternate
????completeWork(current,?completedWork)
????//?省略構造?Effect?List?過程
????//?獲取?Fiber.sibling
????let?siblingFiber?=?workInProgress.sibling
????if?(siblingFiber)?{
??????//?sibling?存在,則跳出?complete?流程,繼續(xù)?beginWork
??????workInProgress?=?siblingFiber
??????return
????}
????completedWork?=?completedWork.return
????workInProgress?=?completedWork
??}
}
遞(beginWork)
先看看 beginWork 進行了哪些操作:
function?beginWork(current,?workInProgress)?{
??if?(current?!==?null)?{?//?current?不為空,表示需要進行?update
????var?oldProps?=?current.memoizedProps?//?原先傳入的?props
????var?newProps?=?workInProgress.pendingProps?//?更新過程中新的?props
????//?組件的?props?發(fā)生變化,或者?type?發(fā)生變化
????if?(oldProps?!==?newProps?||?workInProgress.type?!==?current.type)?{
??????//?設置更新標志位為?true
??????didReceiveUpdate?=?true
????}
??}?else?{?//?current?為空表示首次加載,需要進行?mount
????didReceiveUpdate?=?false
??}
??
??//?tag?表示組件類型,不用類型的組件調用不同方法獲取?child
??switch(workInProgress.tag)?{
????//?函數(shù)組件
????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。
然后會依據(jù) workInProgress.tag 調用不同的方法構建 ?fiber.child。關于 workInProgress.tag 的含義可以參考 react/packages/shared/ReactWorkTags.js,主要是用來區(qū)分每個節(jié)點各自的類型,下面是常用的幾個:
var?FunctionComponent?=?0;?//?函數(shù)組件
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 就是依據(jù) render 的返回值來生成 fiber 節(jié)點并掛載到 workInProgress.child 上。
//?構造子節(jié)點
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
????????)
??????)
????}
????//?返回數(shù)組
????if?(Array.isArray(newChild))?{
??????//?...
????}
????//?返回字符串或數(shù)字,表明是文本節(jié)點
????if?(
??????typeof?newChild?===?'string'?||
??????typeof?newChild?===?'number'
????)?{
??????//?...
????}
????//?返回?null,直接刪除節(jié)點
????return?deleteRemainingChildren(returnFiber,?currentChild)
??}
}
篇幅有限,看看 render 返回值為對象的情況(通常情況下,render 方法 return 的如果是 jsx 都會被轉化為虛擬 DOM,而虛擬 DOM 必定是對象或數(shù)組):
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
}
歸(completeWork)
當 fiber.child 為空時,就會進入 completeWork 流程。而 completeWork 主要就是收集 beginWork 階段設置的 effectTag,如果有設置 effectTag 就表明該節(jié)點發(fā)生了變更, effectTag ?的主要類型如下(默認為 NoEffect ,表示節(jié)點無需進行操作,完整的定義可以參考 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?節(jié)點的?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?節(jié)點
????????//?實例化?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
????????//?更新文本節(jié)點
????????updateHostText(current,?workInProgress,?oldText,?newText)
??????}?else?{
????????//?實例文本節(jié)點
????????workInProgress.stateNode?=?createTextInstance(newText)
??????}
??????return?null
????}
??}
}
與 beginWork 一樣,completeWork 過程中也會依據(jù) workInProgress.tag 來進行不同的處理,其他類型的組件基本可以略過,只用關注下 HostComponent、HostText,這兩種類型的節(jié)點會反應到真實 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)
??//?將發(fā)生變化的屬性放入?updateQueue
??//?注意這里的?updateQueue?不同于?Class?組件對應的?fiber.updateQueue
??workInProgress.updateQueue?=?updatePayload
};
updateHostComponent 方法最后會通過 diffProperties 方法獲取一個更新隊列,掛載到 fiber.updateQueue 上,這里的 updateQueue 不同于 Class 組件對應的 fiber.updateQueue,不是一個鏈表結構,而是一個數(shù)組結構,用于更新真實 DOM。
下面舉一個例子,修改 App 組件的 state 后,下面的 span 標簽對應的 data-val、style、children 都會相應的發(fā)生修改,同時,在控制臺打印出 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}>addbutton>
??????<span
????????data-val={this.state.val}
????????style={{?fontSize:?this.state.val?*?15?}}
??????>
????????{?this.state.val?}
??????span>
????div>)
??}
}
export?default?App
console副作用鏈表
在最后的更新階段,為了不用遍歷所有的節(jié)點,在 completeWork 過程結束后,會構造一個 effectList 連接所有 effectTag 不為 NoEffect 的節(jié)點,在 commit 階段能夠更高效的遍歷節(jié)點。
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>
??}
}
App我們構造一個 2 * 2 的 Table,每次點擊組件,td 的 children 都會發(fā)生修改,下面看看這個過程中的 effectList 是如何變化的。
第一個 td 完成 completeWork 后,EffectList 結果如下:
1第二個 td 完成 completeWork 后,EffectList 結果如下:
2兩個 td 結束了 completeWork 流程,會回溯到 tr 進行 completeWork ,tr 結束流程后 ,table 會直接復用 tr 的 firstEffect 和 lastEffect,EffectList 結果如下:
3后面兩個 td 結束 completeWork 流程后,EffectList 結果如下:
4回溯到第二個 tr 進行 completeWork ,由于 table 已經存在 firstEffect 和 lastEffect,這里會直接修改 table 的 firstEffect 的 nextEffect,以及重新指定 lastEffect,EffectList 結果如下:
5最后回溯到 App 組件時,就會直接復用 table 的 firstEffect 和 lastEffect,最后 的EffectList 結果如下:
6提交更新
這一階段的主要作用就是遍歷 effectList 里面的節(jié)點,將更新反應到真實 DOM 中,當然還涉及一些生命周期鉤子的調用,我們這里只展示最簡單的邏輯。
function?commitRoot(root)?{
??var?finishedWork?=?root.finishedWork
??var?firstEffect?=?finishedWork
??var?nextEffect?=?firstEffect
??//?遍歷effectList
??while?(nextEffect?!==?null)?{
????const?effectTag?=?nextEffect.effectTag
????//?根據(jù)?effectTag?進行不同的處理
????switch?(effectTag)?{
??????//?插入?DOM?節(jié)點
??????case?Placement:?{
????????commitPlacement(nextEffect)
????????nextEffect.effectTag?&=?~Placement
????????break
??????}
??????//?更新?DOM?節(jié)點
??????case?Update:?{
????????const?current?=?nextEffect.alternate
????????commitWork(current,?nextEffect)
????????break
??????}
??????//?刪除?DOM?節(jié)點
??????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 架構下,所有數(shù)據(jù)結構都是鏈表形式,鏈表的遍歷都是通過循環(huán)的方式來實現(xiàn)的,看代碼的過程中經常會被突然出現(xiàn)的 return、break 擾亂思路,所以要完全理解這個流程還是很不容易的。
最后,希望大家在閱讀文章的過程中能有收獲,下一篇文章會開始寫 Hooks 相關的內容。
●?簡單代碼的秘訣
·END·
匯聚精彩的免費實戰(zhàn)教程
喜歡本文,點個“在看”告訴我

