人人都能讀懂的react源碼解析(大廠高薪必備)
5.state更新流程(setState里到底發生了什么)
視頻課程&調試demos
視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler、reconciler、renderer、fiber等,並且詳細debug源碼和分析,過程更清晰。
視頻課程:進入課程
demos:demo
課程結構:
- 開篇(聽說你還在艱難的啃react源碼)
- react心智模型(來來來,讓大腦有react思維吧)
- Fiber(我是在內存中的dom)
- 從legacy或concurrent開始(從入口開始,然后讓我們奔向未來)
- state更新流程(setState里到底發生了什么)
- render階段(厲害了,我有創建Fiber的技能)
- commit階段(聽說renderer幫我們打好標記了,映射真實節點吧)
- diff算法(媽媽再也不擔心我的diff面試了)
- hooks源碼(想知道Function Component是怎樣保存狀態的嘛)
- scheduler&lane模型(來看看任務是暫停、繼續和插隊的)
- concurrent mode(並發模式是什么樣的)
- 手寫迷你react(短小精悍就是我)
上一節我們介紹了react兩種模式的入口函數到render階段的調用過程,也就是mount首次渲染的流程,這節我們介紹在更新狀態之后到render階段的流程。
在react中觸發狀態更新的幾種方式:
-
ReactDOM.render
-
this.setState
-
this.forceUpdate
-
useState
-
useReducer
我們重點看下重點看下this.setState和this.forceUpdate,hook在第11章講
1.this.setState內調用this.updater.enqueueSetState
Component.prototype.setState = function (partialState, callback) { if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) { { throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." ); } } this.updater.enqueueSetState(this, partialState, callback, 'setState'); };
2.this.forceUpdate和this.setState一樣,只是會讓tag賦值ForceUpdate
enqueueForceUpdate(inst, callback) { const fiber = getInstance(inst); const eventTime = requestEventTime(); const suspenseConfig = requestCurrentSuspenseConfig(); const lane = requestUpdateLane(fiber, suspenseConfig); const update = createUpdate(eventTime, lane, suspenseConfig); //tag賦值ForceUpdate update.tag = ForceUpdate; if (callback !== undefined && callback !== null) { update.callback = callback; } enqueueUpdate(fiber, update); scheduleUpdateOnFiber(fiber, lane, eventTime); }, };
如果標記ForceUpdate,render階段組件更新會根據checkHasForceUpdateAfterProcessing,和checkShouldComponentUpdate來判斷,如果Update的tag是ForceUpdate,則checkHasForceUpdateAfterProcessing為true,當組件是PureComponent時,checkShouldComponentUpdate會淺比較state和props,所以當使用this.forceUpdate一定會更新
const shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext, );
3.enqueueForceUpdate之后會經歷創建update,調度update等過程,接下來就來講這些過程
enqueueSetState(inst, payload, callback) { const fiber = getInstance(inst);//fiber實例 const eventTime = requestEventTime(); const suspenseConfig = requestCurrentSuspenseConfig(); const lane = requestUpdateLane(fiber, suspenseConfig);//優先級 const update = createUpdate(eventTime, lane, suspenseConfig);//創建update update.payload = payload; if (callback !== undefined && callback !== null) { //賦值回調 update.callback = callback; } enqueueUpdate(fiber, update);//update加入updateQueue scheduleUpdateOnFiber(fiber, lane, eventTime);//調度update }
狀態更新整體流程
創建Update
HostRoot或者ClassComponent觸發更新后,會在函數createUpdate中創建update,並在后面的render階段的beginWork中計算Update。FunctionComponent對應的Update在第11章講,它和HostRoot或者ClassComponent的Update結構有些不一樣
export function createUpdate(eventTime: number, lane: Lane): Update<*> {//創建update const update: Update<*> = { eventTime, lane, tag: UpdateState, payload: null, callback: null, next: null, }; return update; }
我們主要關注這些參數:
-
lane:優先級(第12章講)
-
tag:更新的類型,例如UpdateState、ReplaceState
-
payload:ClassComponent的payload是setState第一個參數,HostRoot的payload是ReactDOM.render的第一個參數
-
callback:setState的第二個參數
-
next:連接下一個Update形成一個鏈表,例如同時觸發多個setState時會形成多個Update,然后用next 連接
updateQueue:
對於HostRoot或者ClassComponent會在mount的時候使用initializeUpdateQueue創建updateQueue,然后將updateQueue掛載到fiber節點上
export function initializeUpdateQueue<State>(fiber: Fiber): void { const queue: UpdateQueue<State> = { baseState: fiber.memoizedState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null, }, effects: null, }; fiber.updateQueue = queue; }
-
-
baseState:初始state,后面會基於這個state,根據Update計算新的state
- firstBaseUpdate、lastBaseUpdate:Update形成的鏈表的頭和尾
- shared.pending:新產生的update會以單向環狀鏈表保存在shared.pending上,計算state的時候會剪開這個環狀鏈表,並且鏈接在lastBaseUpdate后
- effects:calback不為null的update
從fiber節點向上遍歷到rootFiber
在markUpdateLaneFromFiberToRoot函數中會從觸發更新的節點開始向上遍歷到rootFiber,遍歷的過程會處理節點的優先級(第12章講)
function markUpdateLaneFromFiberToRoot( sourceFiber: Fiber, lane: Lane, ): FiberRoot | null { sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); let alternate = sourceFiber.alternate; if (alternate !== null) { alternate.lanes = mergeLanes(alternate.lanes, lane); } let node = sourceFiber; let parent = sourceFiber.return; while (parent !== null) {//從觸發更新的節點開始向上遍歷到rootFiber parent.childLanes = mergeLanes(parent.childLanes, lane);//合並childLanes優先級 alternate = parent.alternate; if (alternate !== null) { alternate.childLanes = mergeLanes(alternate.childLanes, lane); } else { } node = parent; parent = parent.return; } if (node.tag === HostRoot) { const root: FiberRoot = node.stateNode; return root; } else { return null; } }
調度
在ensureRootIsScheduled中,scheduleCallback會以一個優先級調度render階段的開始函數performSyncWorkOnRoot或者performConcurrentWorkOnRoot
if (newCallbackPriority === SyncLanePriority) {
// 任務已經過期,需要同步執行render階段
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root)
);
} else {
// 根據任務優先級異步執行render階段
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
狀態更新
classComponent狀態計算發生在processUpdateQueue函數中,涉及很多鏈表操作,看圖更加直白
-
初始時fiber.updateQueue單鏈表上有firstBaseUpdate(update1)和lastBaseUpdate(update2),以next連接
-
fiber.updateQueue.shared環狀鏈表上有update3和update4,以next連接互相連接
-
計算state時,先將fiber.updateQueue.shared環狀鏈表‘剪開’,形成單鏈表,連接在fiber.updateQueue后面形成baseUpdate
-
然后遍歷按這條鏈表,根據baseState計算出memoizedState
帶優先級的狀態更新
類似git提交,這里的c3意味着高優先級的任務,比如用戶出發的事件,數據請求,同步執行的代碼等。
-
通過ReactDOM.render創建的應用沒有優先級的概念,類比git提交,相當於先commit,然后提交c3
-
在concurrent模式下,類似git rebase,先暫存之前的代碼,在master上開發,然后rebase到之前的分支上
優先級是由Scheduler來調度的,這里我們只關心狀態計算時的優先級排序,也就是在函數processUpdateQueue中發生的計算,例如初始時有c1-c4四個update,其中c1和c3為高優先級
-
在第一次render的時候,低優先級的update會跳過,所以只有c1和c3加入狀態的計算
-
在第二次render的時候,會以第一次中跳過的update(c2)之前的update(c1)作為baseState,跳過的update和之后的update(c2,c3,c4)作為baseUpdate重新計算
在在concurrent模式下,componentWillMount可能會執行多次,變現和之前的版本不一致
注意,fiber.updateQueue.shared會同時存在於workInprogress Fiber和current Fiber,目的是為了防止高優先級打斷正在進行的計算而導致狀態丟失,這段代碼也是發生在processUpdateQueue中
-