React?中state與props更新深入解析
正文
在這篇文章中,我使用下面這樣的應用程序作為例子
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}; }); } componentDidUpdate() { // todo } render() { return [ <button key="1" onClick={this.handleClick}>Update counter</button>, <span key="2">{this.state.count}</span> ] } }
我給 ClickCounter 組件添加了 componentDidUpdate 鉤子,這個鉤子會在 commit 階段被調用。
在之前我寫了一篇深入介紹 React Fiber的文章,在那篇文章中我介紹了 React 團隊為什么要重新實現 reconciliation 算法、fiber 節(jié)點與 react element 的關系、fiber 節(jié)點的字段以及 fiber 節(jié)點是如何被組織在一起的。在這篇文章中我將介紹 React 如何處理 state 更新以及 React 如何創(chuàng)建 effects list,我也會介紹在 render 階段和 commit 階段調用的函數。
組件的 updater
當我們點擊按鈕之后 handleClick 方法會被調用,這導致 state.count 值加 1
class ClickCounter extends React.Component { ... handleClick() { this.setState((state) => { return {count: state.count + 1}; }); } }
每個 React 組件都有一個相關聯的 updater,它作為組件與 React core 之間的橋梁,這允許 ReactDOM、React Native、服務器端渲染和測試工具以不同的方式實現 setState。這篇文章我們只討論在 ReactDOM 中 updater 的實現,ClickCounter 組件的 updater 是一個 classComponentUpdater,它負責檢索 fiber 實例,隊列更新和工作調度。
const classComponentUpdater = { isMounted, enqueueSetState(inst, payload, callback) { const fiber = ReactInstanceMap.get(inst); const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime, fiber); ... }, enqueueReplaceState(inst, payload, callback) { const fiber = ReactInstanceMap.get(inst); const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime, fiber); ... enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, enqueueForceUpdate(inst, callback) { const fiber = ReactInstanceMap.get(inst); const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime, fiber); ... enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, };
當 update 發(fā)生時,它們被添加到 fiber 節(jié)點上的 updateQueue 中處理。在我們的例子中,ClickCounter 組件的 fiber 節(jié)點的結構如下:
{ stateNode: new ClickCounter, type: ClickCounter, updateQueue: { baseState: {count: 0} firstUpdate: { next: { payload: (state) => { return {count: state.count + 1} } } }, ... }, ... }
如果你仔細觀察,你會發(fā)現 updateQueue.firstUpdate.next.payload 的值是我們在 ClickCounter 組件中傳遞給 setState 的參數。它代表在 render 階段需要處理的第一個 update。
處理 ClickCounter Fiber 的 update
我在上一篇文章中介紹了 nextUnitOfWork 變量的作用,nextUnitOfWork 保存了對workInProgress
樹中 fiber 節(jié)點的引用,當 React 遍歷 fiber 樹時,它使用這個變量來判斷是否有其他未完成工作的 fiber 節(jié)點。
在調用 setState 方法之后,React 把我們傳遞給 setState 的參數添加到 ClickCounter fiber 的 updateQueue 屬性上并且進行工作調度。React 進入 render 階段,它使用renderRoot函數從 fiber 樹最頂層的 HostRoot 開始遍歷 fiber,在遍歷的過程中 React 會跳過已經處理完的 fiber 直到遇到沒有處理的 fiber。在 render 階段,fiber 節(jié)點的所有工作都是在 fiber 的 alternate 字段上進行的。如果還沒有創(chuàng)建 alternate,React 會在處理 update 之前在createWorkInProgress 函數中創(chuàng)建 alternate。
在這里我們假設 nextUnitOfWork 變量中保存的是 ClickCounter fiber 的 alternate。
beginWork
在處理 update 時,首先會調用 beginWork 函數。
由于 fiber 樹上的每一個 fiber 都會執(zhí)行 beginWork 函數,如果你想 debug render 階段,你可以在 beginWork 函數中打斷點。
beginWork 函數基本上包含了一個大的 switch 語句,switch 通過判斷 fiber 的 tag 來確定 fiber 需要做哪些工作
function beginWork(current, workInProgress, ...) { ... switch (workInProgress.tag) { ... case FunctionalComponent: {...} case ClassComponent: { ... return updateClassComponent(current$$1, workInProgress, ...); } case HostComponent: {...} case ... }
由于 ClickCounter 是一個類組件,所以 React 會執(zhí)行updateClassComponent函數,updateClassComponent 函數大概如下:
function updateClassComponent(current, workInProgress, Component, ...) { ... const instance = workInProgress.stateNode; let shouldUpdate; if (instance === null) { ... // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, Component, ...); mountClassInstance(workInProgress, Component, ...); shouldUpdate = true; } else if (current === null) { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...); } else { shouldUpdate = updateClassInstance(current, workInProgress, ...); } return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...); }
在 updateClassComponent 函數中 React 會判斷組件是否是第一次 render、是否是恢復工作或者是否是 update,不同的情況做的事情不一樣。
在上面的例子中,當我們點擊按鈕調用 setState 方法時,我們已經有 ClickCounter 組件實例了,所以 React 會調用updateClassInstance方法,在 updateClassInstance 函數中會按下面的順序執(zhí)行很多函數:
- 調用 UNSAFE_componentWillReceiveProps 鉤子(deprecated)
- 處理 updateQueue 中的 update 并生成新的 state
- 使用這個新 state 調用 getDerivedStateFromProps 并獲得組件最終的 state
- 調用 shouldComponentUpdate 鉤子去確定組件是否需要更新;如果不需要更新就跳過整個 render 階段(不調用組件和組件 children 的 render 方法);否則繼續(xù)更新
- 調用 UNSAFE_componentWillUpdate 鉤子(deprecated)
- 添加觸發(fā) componentDidUpdate 鉤子的 effect
- 更新組件實例的 state 和 props
雖然 componentDidUpdate 鉤子的 effect 是在 render 階段被添加的,但是 componentDidUpdate 鉤子會在 commit 階段執(zhí)行
組件的 state 和 props 會在調用 render 方法之前被更新,因為 render 方法的輸出依賴于 state 和 props 的值。
下面是 updateClassInstance 函數的簡化版本,我刪除了一些輔助代碼
function updateClassInstance(current, workInProgress, ctor, newProps, ...) { const instance = workInProgress.stateNode; const oldProps = workInProgress.memoizedProps; instance.props = oldProps; if (oldProps !== newProps) { callComponentWillReceiveProps(workInProgress, instance, newProps, ...); } let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { processUpdateQueue(workInProgress, updateQueue, ...); newState = workInProgress.memoizedState; } applyDerivedStateFromProps(workInProgress, ...); newState = workInProgress.memoizedState; const shouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...); if (shouldUpdate) { if (typeof instance.componentWillUpdate === 'function') { instance.componentWillUpdate(newProps, newState, nextContext); } if (typeof instance.componentDidUpdate === 'function') { workInProgress.effectTag |= Update; } if (typeof instance.getSnapshotBeforeUpdate === 'function') { workInProgress.effectTag |= Snapshot; } } instance.props = newProps; instance.state = newState; return shouldUpdate; }
在調用生命周期鉤子或添加生命周期鉤子的 effect 之前,React 使用 typeof
檢查實例是否實現了相應的鉤子。例如:React 使用下面的代碼來檢查實例是否有 componentDidUpdate 鉤子:
if (typeof instance.componentDidUpdate === 'function') { workInProgress.effectTag |= Update; }
現在我們大概已經知道了 ClickCounter 的 fiber 節(jié)點在 render 階段要執(zhí)行的操作,現在讓我們看看這些操作是如何改變 fiber 上的值的。調用 setState 之后,當 React 開始工作的時候,ClickCounter 組件的 fiber 節(jié)點像下面這樣:
{ effectTag: 0, elementType: class ClickCounter, firstEffect: null, memoizedState: {count: 0}, type: class ClickCounter, stateNode: { state: {count: 0} }, updateQueue: { baseState: {count: 0}, firstUpdate: { next: { payload: (state, props) => {…} } }, ... } }
當工作完成之后,我們最終得到的 fiber 節(jié)點像這樣:
{ effectTag: 4, elementType: class ClickCounter, firstEffect: null, memoizedState: {count: 1}, type: class ClickCounter, stateNode: { state: {count: 1} }, updateQueue: { baseState: {count: 1}, firstUpdate: null, ... } }
對比 ClickCounter fiber 的前后差異我們可以發(fā)現當 update 被應用之后 memoizedState 和 updateQueue.baseState 中的 count 的值為 1。組件實例中的 state 也會被更新。在這個時候,在隊列中已經沒有 updates 了,所以 firstUpdate 為 null。effectTag 的值不再是 0,它變成了 4,在二進制中,這是 100,這代表了 side-effect 的類型是 Update。
export const Update = 0b00000000100;
總結一下,在處理 ClickCounter fiber 節(jié)點時,React 會調用 pre-mutation 生命周期方法、更新 state 以及定義相關的 side-effects。
Reconciling children for the ClickCounter Fiber
updateClassInstance 運行結束之后,React 會調用finishClassComponent函數,在這個函數中會調用組件的 render 方法,并且在 render 方法返回的 react elements 上運行 diff 算法。diff 算法大概的規(guī)則是:
當比較兩個相同類型的React DOM element 時,React 會檢查這兩個元素的屬性,保持相同的底層 DOM 節(jié)點,只更新已更改的屬性。
Child reconciliation 的過程非常復雜,如果有可能我會單獨寫一篇文章介紹這個過程。在我們的例子中 ClickCounter 的 render 方法返回的是數組,所以在 Child reconciliation 時會調用reconcileChildrenArray。
在這里我們有兩點需要著重理解一下
- 在進行 child reconciliation 時 ,它會為 render 方法返回的 React elements 創(chuàng)建或更新 fiber 節(jié)點。finishClassComponent 返回當前 fiber 的第一個 child,這個返回值會被賦給 nextUnitOfWork 變量并且在之后的 work loop 中處理。
- React 會更新 children 的 props,這是 parent 工作的一部分。
例如,在 React reconciles ClickCounter fiber 的 children 之前,span 元素的 fiber 節(jié)點看上去是這樣的
{ stateNode: new HTMLSpanElement, type: "span", key: "2", memoizedProps: {children: 0}, pendingProps: {children: 0}, ... }
memoizedProps.children 和 pendingProps.children 的值都是 0。從 render 方法中返回的 span 元素的結構如下:
{ $$typeof: Symbol(react.element) key: "2" props: {children: 1} ref: null type: "span" }
對比 span fiber 節(jié)點和 span 元素上的屬性,你會發(fā)現有些屬性值是不同的。createWorkInProgress函數用于創(chuàng)建 fiber 節(jié)點的 alternate,它使用 react element 上最新的 props 和已經存在的 fiber 創(chuàng)建出 alternate。當 ClickCounter 組件完成 children reconciliation 過程之后,span 的 fiber 節(jié)點的 pendingProps 屬性會被更新
{ stateNode: new HTMLSpanElement, type: "span", key: "2", memoizedProps: {children: 0}, pendingProps: {children: 1}, ... }
稍后,當 react 為 span fiber 執(zhí)行工作時,react 會將 pendingProps 復制到 memoizedProps 上并添加更新 DOM 的 effects。
我們已經介紹了 React 在 render 階段為 ClickCounter fiber 節(jié)點執(zhí)行的所有工作。由于按鈕是 ClickCounter 組件的第一個 child,所以它將被分配給 nextUnitOfWork 變量,但是按鈕上沒有需要執(zhí)行工作,所以 React 會快速的移動到按鈕的兄弟節(jié)點上,也就是 span fiber 節(jié)點,這個過程發(fā)生在 completeUnitOfWork 函數中。
處理 Span Fiber 的 update
現在 nextUnitOfWork 中保存的是 span fiber 的 alternate 并且 React 會在它上面開始工作。React 從 beginWork 函數開始,這與處理 ClickCounter 的步驟類似。
因為 span fiber 的類型是 HostComponent,所以在 beginWork 函數中會進入 HostComponent 對應的 switch 分支
function beginWork(current$$1, workInProgress, ...) { ... switch (workInProgress.tag) { case FunctionalComponent: {...} case ClassComponent: {...} case HostComponent: return updateHostComponent(current, workInProgress, ...); case ... }
Reconciling children for the span fiber
在我們的例子中,調用 updateHostComponent 函數時,span fiber 沒有發(fā)生任何重要的改變。
只要 beginWork 函數執(zhí)行完,React 就會開始執(zhí)行 completeWork 函數,但是在執(zhí)行 completeWork 之前 React 會更新 span fiber 上的 memoizedProps,在前面的章節(jié),我提到過在 reconciles children for ClickCounter 時,React 更新了 span fiber 上的 pendingProps,只要 span fiber 在 beginWork 中執(zhí)行完成,React 會將 pendingProps 更新到 memoizedProps 上
function performUnitOfWork(workInProgress) { ... next = beginWork(current, workInProgress, nextRenderExpirationTime); workInProgress.memoizedProps = workInProgress.pendingProps; ... }
在此之后會調用 completeWork,completeWork 函數中是一個大的 switch 語句,由于 span fiber 是 HostComponent,所以會進入 updateHostComponent 函數:
function completeWork(current, workInProgress, ...) { ... switch (workInProgress.tag) { case FunctionComponent: {...} case ClassComponent: {...} case HostComponent: { ... updateHostComponent(current, workInProgress, ...); } case ... } }
在 updateHostComponent 函數中,React 基本上執(zhí)行了如下的操作:
- 準備 DOM 更新
- 將 DOM 更新添加到 span fiber 的 updateQueue 中
- 添加更新 DOM 的 effect
在執(zhí)行這些操作之前,span fiber 看上去是這樣的:
{ stateNode: new HTMLSpanElement, type: "span", effectTag: 0 updateQueue: null ... }
執(zhí)行這些操作之后,span fiber 是這樣的:
{ stateNode: new HTMLSpanElement, type: "span", effectTag: 4, updateQueue: ["children", "1"], ... }
注意 effectTag 和 updateQueue 的值發(fā)生了變化。effectTag 的值從 0 變成了 4,在二進制中,這是 100,這代表了 side-effect 的類型是 Update。updateQueue 字段保存用于 update 的參數。
只要 React 處理完 ClickCounter 和它的 children,render 階段就結束了。
Effects list
在我們的例子中,span fiber 和 ClickCounter fiber 有 side effects,React 會將 HostFiber 的 firstEffect 屬性指向 span fiber。React 在 compliteUnitOfWork函數中創(chuàng)建 effects list,下面是一個帶著 effect 的 fiber tree:
帶有 effect 的線性表是:
commit 階段
commit 階段從 completeRoot函數開始,在開始工作之前先將 FiberRoot.finishedWork 設置為 null
function completeRoot( root: FiberRoot, finishedWork: Fiber, expirationTime: ExpirationTime, ): void { ... // Commit the root. root.finishedWork = null; ... }
與 render 階段不同的是,commit 階段的操作是同步的。在我們的例子中,在 commit 階段會更新 DOM 和調用 componentDidUpdate 生命周期函數,在 render 階段為 span 和 ClickCounter 節(jié)點定義了以下 effect:
{ type: ClickCounter, effectTag: 5 } { type: 'span', effectTag: 4 }
ClickCounter 的 effectTag 為 5,在二進制中為 101,它表示調用組件的 componentDidUpdate 生命周期。span 的 effectTag 為 4,在二進制中為 100,它表示 DOM 更新
應用 effects
讓我們看一下 React 是怎么應用(apply)這些 update 的,應用 effects 是從調用commitRoot函數開始的,這個函數主要調用了如下的三個函數:
function commitRoot(root, finishedWork) { commitBeforeMutationLifecycles() commitAllHostEffects(); root.current = finishedWork; commitAllLifeCycles(); }
在 commitRoot 中調用的這三個函數都實現了一個大的循環(huán),循環(huán)遍歷 effects list 并檢查 effect 的類型。當發(fā)現與函數的用途有關的 effect 時,函數就會應用(apply)它。
由于 commitBeforeMutationLifecycles 的目的是檢查 Snapshot effect 并且調用 getSnapshotBeforeUpdate 方法,但是我們沒有在 ClickCounter 組件上實現這個方法,那么在 render 階段就不會添加 Snapshot effect,所以在我們的例子中 commitBeforeMutationLifecycles 什么都不會做。
effect 類型有:
export const NoEffect = /* */ 0b00000000000; export const PerformedWork = /* */ 0b00000000001; // You can change the rest (and add more). export const Placement = /* */ 0b00000000010; export const Update = /* */ 0b00000000100; export const PlacementAndUpdate = /* */ 0b00000000110; export const Deletion = /* */ 0b00000001000; export const ContentReset = /* */ 0b00000010000; export const Callback = /* */ 0b00000100000; export const DidCapture = /* */ 0b00001000000; export const Ref = /* */ 0b00010000000; export const Snapshot = /* */ 0b00100000000; // Update & Callback & Ref & Snapshot export const LifecycleEffectMask = /* */ 0b00110100100; // Union of all host effects export const HostEffectMask = /* */ 0b00111111111; export const Incomplete = /* */ 0b01000000000; export const ShouldCapture = /* */ 0b10000000000;
DOM updates
執(zhí)行完 commitBeforeMutationLifecycles 之后,React 會調用 commitAllHostEffects函數 ,在 commitAllHostEffects 中 React 會將 span 的文本從 0 變成 1,但是對于 ClickCounter fiber,commitAllHostEffects 什么都不會做,因為類組件的節(jié)點沒有任何 DOM 更新。commitAllHostEffects 函數如下:
function commitAllHostEffects() { while (nextEffect !== null) { ... switch (primaryEffectTag) { case Placement: {...} case PlacementAndUpdate: {...} case Update: { var current = nextEffect.alternate; commitWork(current, nextEffect); break; } case Deletion: {...} } nextEffect = nextEffect.nextEffect; } }
commitAllHostEffects 主要是通過 switch 語句選擇正確的 effect 類型并執(zhí)行相應的操作。在本例中,我們需要更新 span 元素上的文本,因此我們在這里進入 switch 的 Update 分支。在 Update 分支中調用 commitWork 函數,但是實際上最終調用的是 updateDOMProperties
function updateDOMProperties(domElement, updatePayload, ...) { for (let i = 0; i < updatePayload.length; i += 2) { const propKey = updatePayload[i]; const propValue = updatePayload[i + 1]; if (propKey === STYLE) { ...} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...} else if (propKey === CHILDREN) { setTextContent(domElement, propValue); } else {...} } }
updateDOMProperties 接受在 render 階段添加的 updateQueue 作為參數,并且更新 span 元素的 textContent 屬性。
在 commitRoot 函數中,當 DOM 更新之后,在 render 階段生成的workInProgress
被設置為current
root.current = finishedWork;
調用 Post-mutation 生命周期
在 commitRoot 函數中調用的最后一個函數是 commitAllLifeCycles,React 會在這個函數中調用 Post-mutation 生命周期函數。在我們的例子中,在 Render 階段,React 會將 Update effect 添加到 ClickCounter fiber 上,在 commitAllLifeCycles 函數中會檢查 Update effect:
function commitAllLifeCycles(finishedRoot, ...) { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; if (effectTag & (Update | Callback)) { const current = nextEffect.alternate; commitLifeCycles(finishedRoot, current, nextEffect, ...); } if (effectTag & Ref) { commitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; } }
如果有 ref,在 commitAllLifeCycles 中也會更新 ref,但是在我們的例子中沒有 ref,所以 commitAttachRef 不會被調用。
由于我們 ClickCounter fiber 有 Update effect,所以 commitLifeCycles 會被調用,我們定義在組件實例上的 componentDidUpdate 最終是在 commitLifeCycles 中被調用的
function commitLifeCycles(finishedRoot, current, ...) { ... switch (finishedWork.tag) { case FunctionComponent: {...} case ClassComponent: { const instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { if (current === null) { instance.componentDidMount(); } else { ... instance.componentDidUpdate(prevProps, prevState, ...); } } } case HostComponent: {...} case ... }
以上就是React 中state與props更新深入解析的詳細內容,更多關于React中state props更新的資料請關注腳本之家其它相關文章!
相關文章
解決React?hook?'useState'?cannot?be?called?in?
這篇文章主要為大家介紹了React?hook?'useState'?cannot?be?called?in?a?class?component報錯解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12react-native 封裝視頻播放器react-native-video的使用
本文主要介紹了react-native 封裝視頻播放器react-native-video的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-01-01