React 之生命週期實現原理

生命週期是指組件從創建、更新到最終銷燬的完整過程。在不同的階段,React 內置了一些函數以便開發者用來執行不同的邏輯處理。

裝載階段

更新階段

卸載階段

錯誤捕獲

React 的作者 Dan Abramov 畫了一張圖來幫助我們更好地理解 React 的生命週期。

生命週期圖

爲什麼部分生命週期要加 UNSAFE_

React 16 之前,React 的更新都是採用遞歸的方式同步更新。生命週期一但開始,結束結束之前是不會停止的,所以像 componentWillMountcomponentWillReceivePropscomponentWillUpdate 都能順序地正常執行。

而在 React16 中,採用 Fiber + Time Slice 的方式處理每個任務,任務是可以暫停和繼續執行的。這意味着一次完整的執行中,掛載和更新之前的生命週期可能多次執行。所以在 React 16.3.0 中,將 componentWillMountcomponentWillReceivePropscomponentWillUpdate 前面加上了 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 樹是如何生成的? 我們瞭解到,第一個組件、節點都會經歷 beginWorkcompleteWork,首先來看一下,最早執行的 constructor 是在什麼地方實現的。

constructor

beginWork 中判斷當前 workInProgress.tag 的類型,由於 ChildComponenttagClassComponent,所以進入:

// 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;
    }
    // ...
}

這裏我們能看到兩個關鍵方法,constructClassInstancemountClassInstance。從字面意思上看,我們大概能猜出,這裏可能會和 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_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_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;
  }
  // ...
}

它的意思是,如果組件沒有定義過 getDerivedStateFromPropsgetSnapshotBeforeUpdate 方法,並且有定義 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);
  }
}

執行會判斷 oldStateinstance.state 是否相等,如果不相等,就會執行 classComponentUpdater.enqueueReplaceState(instance, instance.state, null);

小結:getDerivedStateFromProps() 的調用位置是 beginWork > updateClassComponent > mountClassInstance > applyDerivedStateFromProps

如果沒有定義 getDerivedStateFromPropsgetSnapshotBeforeUpdate,有定義 componentWillMountUNSAFE_componentWillMount 鉤子會在 beginWork > updateClassComponent > mountClassInstance > callComponentWillMount 調用。

執行完對應的 beginWorkcompleteWork 後,就會進入到 commit 階段。

引用一下 React 的核心思想

const state = reconcile(update);
const UI = commit(state);

通過 const UI = commit(state); 我們可以看出,render 應該是和 commit 階段有關。

render

beginWork 階段執行了 constructorstatic getDerivedStateFromPropsUNSAFE_componentWillMount 生命週期。如何將 Fiber 渲染到頁面上,這就是 render 階段。

具體將 Fiber 渲染到頁面上的邏輯在 commitRootImpl > commitMutationEffects 處。完整的流程可以查看上一篇文章 React 之第一棵樹是如何渲染到頁面上的?,這裏不再贅述。

componentDidMount

commitRootImpl 方法中執行完 commitMutationEffectsFiber 渲染到頁面上後,繼續執行 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 階段,currentnull,更新階段 current 不爲 null。再加上 ChildComponenttype 類型爲 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 中,我們可以訪問更新前的 propsstate 以及舊的 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 FiberdeleteChild 屬性中,並添加 App FiberFlagChildDeletion。這個在 commit 階段,也會遍歷節點,如果發現節點有 deleteChild 值,在 commitMutationEffects 時就會對這個節點進行刪除。

真正執行刪除前,如果組件中有定義 componentWillUnmount,會對 componentWillUnmount 進行調用。調用結束後,會執行 parentInstance.removeChild(child) 將節點從頁面中真正的移除。

完整的執行流程圖如下所示:

卸載執行流程圖

總結

通過上面的學習以及每個步驟的小結,我們知道了 裝載更新卸載 的具體實現原理。但也留下了幾個疑問:

這裏的每一個疑問都值得好好思考,後面再單獨總結。

最後,文章如果有寫得不對的地方,歡迎指正、一起探討!!!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SuyjiMsIxnCsNUCh6_3Nhg