iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 12
1
Modern Web

I Want To Know React系列 第 12

I Want To Know React - Lifecycle 階段

回顧 React Lifecycle

在上個章節中,我們了解了 React component instance 的生命週期(Lifecycle)代表一個 component instance 各個生命階段。

另外,React 在每個生命週期都有提供對應的生命週期函式(Lifecycle methods),讓開發者在 component instance 對應的時期執行函式內容。

Component instance 生命週期有三個:

  • Mounting (創建) 時期

    發生在 component 的 instance 剛建立出來,並被加入 DOM 中的時候

  • Updating (更新) 時期

    在 component instance 的 props 或 state 被更新,亦或者被強制更新時觸發

  • Unmounting (刪除) 時期

    在 component 的 instance 要被刪除,並從 DOM 裡移除時觸發

如 Lifeycle 概覽圖所示:

https://ithelp.ithome.com.tw/upload/images/20200927/20107790BQJxVB7qa3.png

在本章節中,我們則要換個角度,專注在研究每個生命周期三個子階段的功能與特性:

  • Render 階段
  • Pre-commit 階段
  • Commit 階段

相對於 lifecycle 的各個時期是對應的概覽圖的一排(Column),lifecycle 的各個子階段則是對應到 概覽圖的每一列(Row)。讀者閱讀時也可以同時參考概覽圖。

Render 階段

觸發時機

Render 階段代表 React 產出 React element 並決定有哪些 element 要被加到 DOM 中的階段。

Render 階段會發生在兩個時間週期:Mounting 時期Updating 時期的一開始。換句話說,Mounting 與 Updating 被觸發時,首先都會先執行 Render 階段。如下圖紅色橫列所示:

https://ithelp.ithome.com.tw/upload/images/20200927/20107790EPucZfW5Do.png

內部運作步驟

具體一點來說,React 在 Render 階段大致上會做以下幾個步驟:

  1. 初始化 Component instance(Mount 時期 only)

    如果是在 Mounting 時期,React render 階段就會執行 class 的 constructor 初始化並將 component instance 創建出來。

  2. 決定 Component 是否更新(Updating 時期 only)

    在 Updating 時期,React 會透過 shouldComponentUpdate 讓開發者決定一個 component instance 是否該 update。

  3. 產出 Component 自定義的 React element(Mount 時期 & Updating 時期)

    接著,React 會再次執行 render 以產出 Component 自定義的 React element 內容。此內容通樣尚未實際放到 DOM 上。

  4. 把 Virtual DOM snapshot 產出來,並跟之前的 Virtual DOM snapshot 比較

    React 會在把 Virtual DOM snapshot 產出來,並與上一個產出的 Virtual DOM snapshot 做比較(Diff),決定哪些內容是要實際被更新到 DOM 上的。

從上個段落可以看到,3 4 步已經涵蓋在上一篇提到的 Mounting 時期與 Updating 時期中,這邊只是換個角度來看而已。

Render 階段的目的

了解上面的運作原理步驟後,我們可以知道 Render 階段的重點就在於:

  • 決定哪些內容要實際 render 到 DOM 上。

決定的方法可能是透過 render 產出 React element 與 VirtualDOM snapshot diff,亦或者透過 shouldComponentUpdate 來決定 instance 要不要更新。也就是說,此階段會是純 JavaScript 的操作,與實際畫面(DOM)完全無關。

特性:可能會被終止、暫停、重新執行

如同概覽圖所寫的:

"Render Phase": May be paused, aborted or restarted by React.

,Render 階段是有可能被 React 終止、暫停、重新執行的。

Render Phase 可能被終止、暫停、重新執行的原因是因為 React 要實現批次(Batch)或 非同步(Async) render 來提高效能。

相關 lifecycle methods 規定:必須是 pure function

由剛剛提到的特性可以知道,render 階段的 lifecycle methods 是可能被多次執行的,因此 render 階段的 lifecycle methods 都不應該有 side effect,否則可能發生記憶體洩漏(Memory leak)或是資料錯亂等非預期且難以穩定重現的 bug。

舉例來說,如果我們在 render 階段的 lifecycle 中對後端發出 API 請求,則實際上在 render 時,此 API 就可能非預期的被呼叫多次。

也就是說以下這些 lifecycle methods 都必須為 pure function:

  • constructor
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • setState updater functions (第一個參數)

另外需要注意的一點是,在 event 觸發的函式中是可以有 side effect 的。雖然 event 的函式呼叫位置是在 render 中,但事實上,觸發 event 的時間點已經脫離 render 階段了,所以執行 side effect 是沒問題的。

Pre-commit 階段

觸發時機

Pre-commit 階段發生在 React 要把 element 加到 DOM 的前一刻。

Pre-commit 階段只會發生在 Updating 時期 而已。如下圖紅色橫列所示:

https://ithelp.ithome.com.tw/upload/images/20200927/20107790ueD7PVPQSQ.png

內部運作步驟

具體一點來說,React 在 Pre-commit 階段只會做以下這個步驟:

  1. 執行 getSnapshotBeforeUpdate

    這個 lifecycle method 是用來讓開發者抓取改動前的 DOM 資訊並計算相關資料的函式(例如抓取 scrollbar 位置),React 本身則沒有做其他額外的事情。

    筆者猜測這也是為何生命週期概覽圖上的 Pre-commit 階段只用一條線代表的原因。

Pre-commit 階段的目的

如剛剛所述,Pre-commit 階段的目的主要就是提供一個 lifecycle 時期的 callback,以讓開發者讀取改動前的 DOM,並透過前一刻的 props 與 state 來計算相關內容。

相關 Lifecycle methods 規定:用來讀取 DOM

由於這個階段,只是用來讀取 DOM 並計算、回傳相關的值而已,因此不推薦在這邊做其他的 side effect。

Commit 階段

觸發時機

Commit 階段代表把元件實際更新到 DOM 上的階段。

Commit 階段在三個時間週期都會被觸發:Mounting 時期與 Updating 時期的 render 之後,以及 Unmounting 實際刪除元件之前。如下圖紅色橫列所示:

https://ithelp.ithome.com.tw/upload/images/20200927/201077906b2QWCOTWr.png

內部運作步驟

具體一點來說,React 在 Commit 階段只會做以下這個步驟:

  1. React 會實際把 Virtual DOM 的 diff 更新到 DOM 上(Mounting 時期 & Updating 時期)

    Render 階段段落所提,render 階段完成後會產出一份 Virtual DOM 的 Diff,代表實際要更新到畫面上的內容。Commit 階段則是負責把這些內容更新到 DOM 上。

  2. 呼叫對應的 lifecycle methods

    Commit 階段也會呼叫對應的 lifecycle methods 作為特定時期的 callback 使用。Mounting 時期與 Updating 時期都是在內容更新到 DOM 上後呼叫 lifecycle methods,只有 Unmouning 時期是在元件被刪除之前呼叫。

Commit 階段的目的

了解上面的運作原理步驟後,我們可以知道 Commit 階段的重點就在於:

  • 把要更新的內容實際畫到 DOM 上

特性:不會被終止、暫停、重新開始

如剛剛提到,commit 階段是確定要更新的內容後才去修改畫面的,因此 React 不會終止、暫停或重新開始 commit 階段。

相關 Lifecycle methods 規定:可執行 side effect & 可操作 DOM

由於不會被終止、暫停或重新開始的特性,因此 commit 階段是允許做 side effect 的,因為在這階段, side effect 不會像是 render 階段一樣被意外的執行多次。

換句話說,Commit 階段的 lifecycle methods 就是三個階段中唯一被允許執行 side effect 的階段,舉例來說如果需要呼叫後端 API 或是 setState 的話,都可以在這一階段做。

另外,因為這一階段的 lifecycle 也已經把變更更新到 DOM 上了,因此在這階段中可以正常的操作 DOM。

Commit 階段的 lifecycle methods 包括:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

小結

在這個章節中,我們學到了 lifecycle 中的三個子階段各自的功能與特性:

  • Render 階段
    • 功能:產出 Virtual DOM Diff
    • 特性:可能被終止、暫停、重新開始
    • Lifecycle methods 實作規定:必須為 pure function,不能有 side effect
  • Pre-commit 階段
    • 功能:提供開發者讀 DOM 的 callback
    • Lifecycle methods 實作規定:用來讀 DOM,不應該有其他 side effect
  • Commit 階段
    • 功能:把元件更新實際更新到 DOM 上
    • 特性:不會被終止、暫停、重新開始
    • Lifecycle methods 實作規定:可執行 side effect,包括呼叫後端 API、setState、讀 DOM

參考資料

由於筆者認為深入介紹 lifecycle 的文件較為瑣碎,散落在不同的參考資料中,因此將擷取原文一併附在底下的段落中。

Lifecycle 相關

Render 階段相關

Commit 階段相關

  • https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

    The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.) React also calls lifecycles like componentDidMount and componentDidUpdate during this phase.

    The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming concurrent mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

  • https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

    Can work with DOM, run side effects, schedule updates


上一篇
I Want To Know React - 初探 Lifecycle
下一篇
I Want To Know React - 處理 Event
系列文
I Want To Know React30

1 則留言

0
Dylan
iT邦新手 5 級 ‧ 2020-11-08 17:11:04

想請教大大,
若要證明 render 階段特性的「終止、暫停、重新開始」,code 該如何呢 ?

我要留言

立即登入留言