React 之生命週期實現原理
生命週期是指組件從創建、更新到最終銷燬的完整過程。在不同的階段,React
內置了一些函數以便開發者用來執行不同的邏輯處理。
裝載階段
-
constructor
-
static getDerivedStateFromProps
-
UNSAFE_componentWillMount
-
render
-
componentDidMount
更新階段
-
UNSAFE_componentWillReceiveProps
-
static getDerivedStateFromProps
-
shouldComponentUpdate
-
UNSAFE_componentWillUpdate
-
render
-
getSnapshotBeforeUpdate
-
componentDidUpdate
卸載階段
componentWillUnMount
錯誤捕獲
-
static getDerivedStateFromError
-
componentDidCatch
React
的作者 Dan Abramov
畫了一張圖來幫助我們更好地理解 React
的生命週期。
爲什麼部分生命週期要加 UNSAFE_
在 React 16
之前,React
的更新都是採用遞歸的方式同步更新。生命週期一但開始,結束結束之前是不會停止的,所以像 componentWillMount
、componentWillReceiveProps
、componentWillUpdate
都能順序地正常執行。
而在 React16
中,採用 Fiber
+ Time Slice
的方式處理每個任務,任務是可以暫停和繼續執行的。這意味着一次完整的執行中,掛載和更新之前的生命週期可能多次執行。所以在 React 16.3.0
中,將 componentWillMount
、componentWillReceiveProps
、componentWillUpdate
前面加上了 UNSAFE_
,一來是爲了漸進式升級 React
,不能一刀切。二來來提示使用者,這幾個方法在新版本的 React
中使用起來可能會存在一些風險,建議使用最新的生命週期來開發,以防止出現一些意外的 bug
。
對於不同生命週期的作用,想必大家都有所瞭解,不清楚的可以查看 官方文檔 https://zh-hans.reactjs.org/docs/react-component.html 進行學習。
接下來,我們從一個簡單的例子入手,順着它的執行來看看生命週期在源碼中是如何實現的。
Demo 組件代碼
初始化一個簡單組件,組件會展示一個數字和一個按鈕,每次點擊按鈕時,數字都會 + 1
。
// App.jsx
import { useState } from 'react';
import ChildComponent from './ChildComponent';
function App() {
const [show, setShow] = useState(true);
return (
<>
{ show && <ChildComponent count={count} />}
<button onClick={() => setShow(pre => !pre)}>toggle</button>
</>
)
}
export default App;
// ChildComponent.jsx
import { Component } from 'react';
export default class ChildComponent extends Component {
constructor(props) {
super(props);
this.state = {
num: 1
}
}
render() {
const { num } = this.state;
return (
<>
<h1>{num}</h1>
<button onClick={() => this.setState({ num: num + 1 })}>點擊我+1</button>
</>
)
}
}
效果截圖如下所示:
當首次進來時,會進入裝載 Mounting
階段。
裝載階段
從 第一棵 Fiber 樹是如何生成的? 我們瞭解到,第一個組件、節點都會經歷 beginWork
、completeWork
,首先來看一下,最早執行的 constructor
是在什麼地方實現的。
constructor
beginWork
中判斷當前 workInProgress.tag
的類型,由於 ChildComponent
的 tag
是 ClassComponent
,所以進入:
// packages\react-reconciler\src\ReactFiberBeginWork.old.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...
switch (workInProgress.tag) {
// ...
case ClassComponent: {
// ...
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
}
// ...
}
這個方法是返回 updateClassComponent
方法執行後的返回值。
// packages\react-reconciler\src\ReactFiberBeginWork.old.js
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
// ...
const instance = workInProgress.stateNode;
let shouldUpdate;
// 根據組件 stateNode(組件實例)的值是否爲 null,以此來判斷應該創建組件還是更新組件
if (instance === null) {
// In the initial pass we might need to construct the instance.
// 實例化組件,將組件實例與對應的 fiber 節點關聯
constructClassInstance(workInProgress, Component, nextProps);
// 將 fiber 上的 state 和 props 更新至組件上
// 並且會檢查是否聲明瞭 getDervedStateFromProps 生命週期
// 有的話則會調用並且使用 getDerivedStateFromProps 生命週期函數中返回的 state 來更新組件實例上的 state
// 檢查是否聲明瞭 componentDidMount 生命週期,有的話則會收集標示添加到 fiber 的 flags 屬性上
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
// 創建組件肯定是需要更新的,所以直接爲 shouldUpdate 賦值爲 true
shouldUpdate = true;
}
// ...
}
這裏我們能看到兩個關鍵方法,constructClassInstance
、mountClassInstance
。從字面意思上看,我們大概能猜出,這裏可能會和 React
生命週期的 constructor
有關。
// packages\react-reconciler\src\ReactFiberClassComponent.old.js
function constructClassInstance(
workInProgress: Fiber,
ctor: any, // ChildComponent
props: any,
): any {
// ...
// 實例化組件
let instance = new ctor(props, context);
// 將獲取到的組件上的 state 屬性複製給 workInProgress.memoizedState
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined
? instance.state
: null);
// 將 fiber 節點與組件實例相互關聯,在之前更新時可複用
adoptClassInstance(workInProgress, instance);
// ...
if (__DEV__) {
if (
typeof ctor.getDerivedStateFromProps === 'function' ||
typeof instance.getSnapshotBeforeUpdate === 'function'
) {
if (
foundWillMountName !== null ||
foundWillReceivePropsName !== null ||
foundWillUpdateName !== null
) {
const componentName = getComponentNameFromType(ctor) || 'Component';
const newApiName =
typeof ctor.getDerivedStateFromProps === 'function'
? 'getDerivedStateFromProps()'
: 'getSnapshotBeforeUpdate()';
if (!didWarnAboutLegacyLifecyclesAndDerivedState.has(componentName)) {
didWarnAboutLegacyLifecyclesAndDerivedState.add(componentName);
console.error(
'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
'%s uses %s but also contains the following legacy lifecycles:%s%s%s\n\n' +
'The above lifecycles should be removed. Learn more about this warning here:\n' +
'https://reactjs.org/link/unsafe-component-lifecycles',
componentName,
newApiName,
foundWillMountName !== null ? `\n ${foundWillMountName}` : '',
foundWillReceivePropsName !== null
? `\n ${foundWillReceivePropsName}`
: '',
foundWillUpdateName !== null ? `\n ${foundWillUpdateName}` : '',
);
}
}
}
// ...
return instance;
}
這個方法中,調用 new ctor(props, context)
方法,向上找 ctor
是什麼?在 beginWork
對就的 case ClassComponent
中可以看到,ctor
其它就是 const Component = workInProgress.type;
,而 workInProgress.type
指向的就是 ChildComponent
這個 class
。
通過 new ctor(props, context)
新建一個 class
的實例,自然,也就會執行這個 class
對應的 constructor
構造函數了。
此外,在開發環境中,如果我們使用了 getDerivedStateFromProps
或者 getSnapshotBeforeUpdate
,同時又使用了 UNSAFE_componentWillMount
、UNSAFE_componentWillReceiveProps
、UNSAFE_componentWillUpdate
鉤子方法,控制檯中會進行報錯提示,這也是在 constructClassInstance
方法中執行的。
小結:
constructor
的執行位置是:beginWork > updateClassComponent > constructClassInstance
。
getDerivedStateFromProps & UNSAFE_componentWillMount
執行完 constructor
生命週期後,繼續執行 mountClassInstance(workInProgress, Component, nextProps, renderLanes);
。
// packages\react-reconciler\src\ReactFiberClassComponent.old.js
function mountClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): void {
// ...
// 檢查當前組件是否聲明瞭 getDerivedStateFromProps 生命週期函數
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
// 有聲明的話則會調用並且使用 getDerivedStateFromProps 生命週期函數中返回的 state 來更新 workInProgress.memoizedState
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
// 將更新了的 state 賦值給組件實例的 state 屬性
instance.state = workInProgress.memoizedState;
}
}
這個方法會判斷組件中是否定義了 getDerivedStateFromProps
,如果有就會執行 applyDerivedStateFromProps
方法:
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
在 applyDerivedStateFromProps
這個方法中,會調用組件的 getDerivedStateFromProps
方法,將方法的返回值賦值給 workInProgress.memoizedState
,具體的實現方法如下所示:
// packages\react-reconciler\src\ReactFiberClassComponent.old.js
function applyDerivedStateFromProps(
workInProgress: Fiber,
ctor: any,
getDerivedStateFromProps: (props: any, state: any) => any,
nextProps: any,
) {
const prevState = workInProgress.memoizedState;
let partialState = getDerivedStateFromProps(nextProps, prevState);
// Merge the partial state and the previous state.
const memoizedState =
partialState === null || partialState === undefined
? prevState
: assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;
// ...
}
在 mountClassInstance
方法中,還有這樣的判斷
// packages\react-reconciler\src\ReactFiberClassComponent.old.js
function mountClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): void {
// ...
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
// 調用 componentWillMount 生命週期
if (
typeof ctor.getDerivedStateFromProps !== 'function' &&
typeof instance.getSnapshotBeforeUpdate !== 'function' &&
(typeof instance.UNSAFE_componentWillMount === 'function' ||
typeof instance.componentWillMount === 'function')
) {
callComponentWillMount(workInProgress, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
}
// 判斷是否聲明瞭 componentDidMount 聲明週期,聲明瞭則會添加標識 Update 至 flags 中,在 commit 階段使用
if (typeof instance.componentDidMount === 'function') {
const fiberFlags: Flags = Update | LayoutStatic;
workInProgress.flags |= fiberFlags;
}
// ...
}
它的意思是,如果組件沒有定義過 getDerivedStateFromProps
、getSnapshotBeforeUpdate
方法,並且有定義 componentWillMount
|| UNSAFE_componentWillMount
方法,就會調用 callComponentWillMount
去執行 componentWillMount
|| UNSAFE_componentWillMount
方法。
function callComponentWillMount(workInProgress, instance) {
const oldState = instance.state;
if (typeof instance.componentWillMount === 'function') {
instance.componentWillMount();
}
if (typeof instance.UNSAFE_componentWillMount === 'function') {
instance.UNSAFE_componentWillMount();
}
if (oldState !== instance.state) {
classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
}
}
執行會判斷 oldState
和 instance.state
是否相等,如果不相等,就會執行 classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
。
小結:
getDerivedStateFromProps()
的調用位置是beginWork > updateClassComponent > mountClassInstance > applyDerivedStateFromProps
。如果沒有定義
getDerivedStateFromProps
、getSnapshotBeforeUpdate
,有定義componentWillMount
、UNSAFE_componentWillMount
鉤子會在beginWork > updateClassComponent > mountClassInstance > callComponentWillMount
調用。
執行完對應的 beginWork
和 completeWork
後,就會進入到 commit
階段。
引用一下 React
的核心思想
const state = reconcile(update);
const UI = commit(state);
通過 const UI = commit(state);
我們可以看出,render
應該是和 commit
階段有關。
render
beginWork
階段執行了 constructor
、static getDerivedStateFromProps
、UNSAFE_componentWillMount
生命週期。如何將 Fiber
渲染到頁面上,這就是 render
階段。
具體將 Fiber
渲染到頁面上的邏輯在 commitRootImpl > commitMutationEffects
處。完整的流程可以查看上一篇文章 React 之第一棵樹是如何渲染到頁面上的?,這裏不再贅述。
componentDidMount
在 commitRootImpl
方法中執行完 commitMutationEffects
將 Fiber
渲染到頁面上後,繼續執行 commitRootImpl
方法中的 commitLayoutEffects
方法。
// packages\react-reconciler\src\ReactFiberCommitWork.old.js
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
// ...
commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
}
commitLayoutEffects
方法裏執行 commitLayoutEffectOnFiber
方法。
// packages\react-reconciler\src\ReactFiberCommitWork.old.js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
// ...
case ClassComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
commitClassLayoutLifecycles(finishedWork, current);
}
// ...
}
// ...
}
經歷過一些判斷以及遍歷,最後會進入 case ClassComponent
階段的 commitClassLayoutLifecycles
中。
// packages\react-reconciler\src\ReactFiberCommitWork.old.js
function commitClassLayoutLifecycles(
finishedWork: Fiber,
current: Fiber | null,
) {
const instance = finishedWork.stateNode;
if (current === null) {
if (shouldProfile(finishedWork)) {
// ...
} else {
try {
instance.componentDidMount();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
if (shouldProfile(finishedWork)) {
// ...
} else {
try {
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
}
從這個方法中可以看出,如果 current === null
(首次加載),就會調用 instance.componentDidMount();
。如果 current !== null
(更新),就會調用 instance.componentDidUpdate
。
小結:
componentDidMount
的執行位置是:commitRoot > commitRootImpl > commitLayoutEffects > commitLayoutEffectOnFiber > recursivelyTraverseLayoutEffects > commitClassLayoutLifecycles
。
完成的代碼執行流程圖如下所示:
到目前爲止,裝載階段的生命週期就完成了,下面,來看一下更新階段的生命週期是如何實現的。
更新階段
當我們點擊 Demo
中的 點擊我+1
按鈕,數字 1
將變成 2
,在此期間,會怎樣執行呢?
通過 setState
觸發 React
更新。React
會從 FiberRoot
開始進行處理。
提個問題:爲什麼每次更新
React
都要從根節點開始執行?它是如何保證性能的?這樣做的原因是什麼?爲什麼它不從更新的組件開始?這個問題先提到這裏,後面再單獨總結。
爲方便理解,這裏將代碼的執行流程做成了一個流程圖,具體如下所示:
當 workInProgress
經過 workLoop
遍歷到 ChildComponent
時,又會開始進入它的 beginWork
。通過之前的學習,我們瞭解到,在 Mounting
階段,current
爲 null
,更新階段 current
不爲 null
。再加上 ChildComponent
的 type
類型爲 ClassComponent
,所以 ChildComponent
會執行 updateClassComponent
方法。
執行 updateClassInstance
方法,在這個方法中會判斷組件中是否定義了 getDerivedStateFromProps
或者 getSnapshotBeforeUpdate
。如果沒有定義,纔會檢查是否有定義 *_componentWillReceiveProps
方法並執行它。如果這兩種都存在,在 Mounting
階段執行 constructClassInstance
就會打印 error
信息。
繼續執行後面的代碼,發現如果組件中有定義 getDerivedStateFromProps
就會執行 getDerivedStateFromProps
方法,將方法的返回值掛載到 workInProgress.memoizedState
,所以這個方法可以用來在渲染前根據新的 props
和舊的 state
計算衍生數據。
執行完 getDerivedStateFromProps
就會開始檢查是否定義了 shouldComponentUpdate
,如果有定義,執行 shouldComponentUpdate
方法,並將方法的返回值用於判斷頁面是否需要更新的依據。如果返回 true
,說明需要更新,如果此時組件中有定義 componentWillUpdate
也會執行它,然後根據條件修改 workInProgress.flags
的值爲 Update
或者 Snapshot
。在 commit
階段,會根據不同的 flags
對組件進行不同的處理。
調用 render
方法,拿到當前組件的子節點內容。
在將新的內容 commitMutationEffects
(將新節點渲染到頁面之前),調用 getSnapshotBeforeUpdate
。所以在 getSnapshotBeforeUpdate
中,我們可以訪問更新前的 props
和 state
以及舊的 DOM
節點信息,並且它的返回值會綁定到當前 Fiber
節點的 __reactInternalSnapshotBeforeUpdate
屬性上面,這個參數會作爲後面的 componentDidUpdate
鉤子的第三個參數調用。
這個特性在處理渲染頁面前,需要獲取之前的頁面信息,並根據之前的頁面信息執行一些新的交互上有奇效。比如:收到新消息時,需要自動滾動到新消息處。
執行完 getSnapshotBeforeUpdate
,繼續執行 commitLayoutEffects
,然後在 commitLayoutEffectOnFiber
裏面經過不同的 case
調用 recursivelyTraverseLayoutEffects
方法,這個方法又會將 parentFiber.child
作爲參數繼續調用 commitLayoutEffectOnFiber
,直到找到 ChildComponent
。執行完 case ClassComponent
裏面的 recursivelyTraverseLayoutEffects
方法,就會開始調用 commitClassLayoutLifecycles
方法,這個方法中就會判斷,如果有定義 componentDidUpdate
就會執行它。如果有執行 getSnapshotBeforeUpdate
,還會將它的返回值作爲第三個參數傳給 componentDidUpdate
來執行。
小結:到目前爲止,更新階段就執行完了。有一些不再維護的生命週期,會根據組件中是否有定義最新的一些生命週期來判斷是否需要執行。
卸載階段
當點擊 Demo
中的 toggle
按鈕時,會觸發 setShow(false)
。此時頁面的展示代碼如下:
<div style={{ padding: 20 }}>
{ false && <ChildComponent count={count} />}
<button onClick={() => setShow(pre => !pre)}>toggle</button>
</div>
React
又開始對每個節點進行 beginWork
。當遍歷到 App
節點時,它下面有兩個子節點,false
& <button>
按鈕。進入 reconcileChildrenArray
進行 diff
算法的比較。當比較 { false && <ChildComponent count={count} />}
時,發現這個節點的值是 boolean
,它是一個條件判斷值,和需要渲染的任何類型都不相關,此時,會將這個節點對應的原來的 <ChildComponent count={count} />
添加到它的 returnFiber
也就是 App Fiber
的 deleteChild
屬性中,並添加 App Fiber
的 Flag
爲 ChildDeletion
。這個在 commit
階段,也會遍歷節點,如果發現節點有 deleteChild
值,在 commitMutationEffects
時就會對這個節點進行刪除。
真正執行刪除前,如果組件中有定義 componentWillUnmount
,會對 componentWillUnmount
進行調用。調用結束後,會執行 parentInstance.removeChild(child)
將節點從頁面中真正的移除。
完整的執行流程圖如下所示:
總結
通過上面的學習以及每個步驟的小結,我們知道了 裝載
、更新
、卸載
的具體實現原理。但也留下了幾個疑問:
-
爲什麼每次更新都要從
FiberRoot
節點開始? -
錯誤處理是如何實現的?
-
diff
算法的具體實現邏輯是什麼? -
現在都推薦使用
Hooks
寫法,Class
寫法以及它的生命週期還有什麼作用,還需要這樣深入學習嗎? -
Hooks
的生命週期是如何實現的?
這裏的每一個疑問都值得好好思考,後面再單獨總結。
最後,文章如果有寫得不對的地方,歡迎指正、一起探討!!!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/SuyjiMsIxnCsNUCh6_3Nhg