React 元件生命週期

揹包の技術發表於2023-12-08

求上進的人,不要總想著靠誰,人都是自私的,自己才是最靠得住的人。

React 中生命週期劃時代幾個節點,React 16.2 之前處於老的生命週期,之後提出了新的生命週期。而函式式元件在 React 16.8 之前是沒有狀態和生命週期的,在 React 16.8 版本透過引入 Hooks 使得函式式元件也能有狀態和生命週期了。

1. 初始化階段

1.1 componentWillMount:

元件即將掛載,初始化資料作用,即 render 之前最後一次修改狀態的機會。

// 元件即將掛載
componentWillMount() {
  // 初始化資料作用
  console.log("componentWillMount")
}

/* 在 16.2 之後版本使用會出現以下警告 ⚠️⚠️⚠️
  react-dom.development.js:86 Warning: componentWillMount has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.

  * Move code with side effects to componentDidMount, and set initial state in the constructor.
  * Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.

  Please update the following components: App
  */

// 元件即將掛載 - 強制去掉警告,UNSAFE 提示開發者這是一個不安全的生命週期方法。
UNSAFE_componentWillMount() {
  // 初始化資料作用
  console.log("componentWillMount")
}

componentWillMount 在16.2 之後官方不推薦使用了,這是因為 16.2 的時候 React 發生了一個改變,推出了幾個新的生命週期,老的生命週期方法被替代掉了,不推薦使用。 那麼為什麼 React 要推出新的生命週期呢?

在 React 16.2 中透過對 diff 演演算法的更新,更加最佳化它的效能。提出了一個 Fiber 技術(纖維、分片、切片,比執行緒更小的一種概念)。因為我們傳統的 React 它在建立、更新狀態之後會建立新的虛擬 Dom 樹,會對比老的虛擬 Dom 樹,這個過程是同步的,如果資料量比較小還好。如果這個資料量非常多的情況下即元件非常多的情況下(例如:幾百個元件),這個時候更新操作,會導致我們瀏覽器假死、卡頓,這個時候點什麼都沒有反應,因為它忙著新老虛擬 Dom 的對比,就是它在對比兩個超級大的物件,裡麵包含了很多小物件,這時瀏覽器無法處理其它事件,所以導致卡頓影響體驗。

所以這個東西就是一個邊緣化的問題,你的元件達到這樣一個程度,它真的會出現假死的情況。所以 React Fiber 技術就是來最佳化了虛擬 Dom 的 diff 演演算法,它把建立 Dom 和元件渲染整個過程拆分成了無數個小的分片任務來執行。可以認為這個任務無比的碎片化,這個時候如果有優先順序較高的任務就先執行優先順序較高的任務。當優先順序較低的任務在執行時候突然來了優先順序較高的任務,這個時候會打斷正在執行的低優先順序任務,先執行優先順序高的任務。所謂的低優先順序任務就是 componentWillMount 中去找哪些節點將要去掛載到頁面中,而高優先順序任務就是 render 正在渲染,didMount 掛載完成。這個時候我們低優先順序的任務(找出那些需要更新的 Dom)這個過程是會被打斷的,而我們更新 Dom 這個過程是不能被打斷的,只能一鼓作氣做完的,而 willMount 很不幸它是處在這個要找出來那些 Dom 是需要被更新的。所以這個過程是可以被打斷的,所以可以認為 willMount 在忙著找出那些狀態需要更新。因為接下來在 render 中就要開始更新了,didMount 就更新完成了。這個時候 willMount 找是處於低優先順序的,而這個時候 render 正在更新,因為碎片化任務,他可能還不是同步的。即某個元件可能處在在找那個狀態需要更新,那個 Dom 需要更新,而那邊元件已經到了 render 渲染部分了,這個時候就吧低優先順序的任務給砍掉了。砍掉怎麼辦,會儲存嗎?不會。只能下次再來一遍,再來找那個節點需要更新。所以這個生命週期就可能會觸發多次這樣一個問題(失去了唯一性),所以這是一個有隱患的生命週期方法,所以這裡不推薦使用。

1.2 render

元件正式掛載渲染,只能訪問 this.props 和 this.state,不允許修改狀態和 Dom 輸出。

1.3 componentDidMount

元件掛載完成,成功 render 並渲染完成真實 DOM 之後觸發,可以訪問、修改 Dom。

componentDidMount() {
  // 資料請求axios
  // 訂閱函式呼叫
  // setInterval
  // 基於建立的完的dom進行初始化時候,例如 BetterScroll 使用
  console.log("componentDidMount")
}

2. 執行中階段

2.1 componentWillUpdate

元件即將更新,不能修改屬性和狀態,會造成死迴圈。非安全被棄用,同 componentWillMount。

// 元件即將更新
componentWillUpdate() {
  console.log("componentWillUpdate")
}

/* 在 16.2 之後版本使用會出現以下警告 ⚠️⚠️⚠️
  react-dom.development.js:86 Warning: componentWillUpdate has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.

* Move data fetching code or side effects to componentDidUpdate.
* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.

Please update the following components: App
  */

// 元件即將更新 - 強制去掉警告,UNSAFE 提示開發者這是一個不安全的生命週期方法。
 UNSAFE_componentWillUpdate() {
  console.log("componentWillUpdate")
}

2.2 render

元件正式掛載渲染,只能訪問 this.props 和 this.state,不允許修改狀態和 Dom 輸出。

2.3 componentDidUpdate

元件更新完成,成功 render 並渲染完成真實 DOM 之後觸發,可以訪問、修改 Dom。

// 元件更新完成 - 接收兩個行參,老的屬性、老的狀態
componentDidUpdate(prevProps, prevState) {
  console.log(prevState)
  console.log("componentDidUpdate")
}

2.4 shouldComponentUpdate

scu 控制元件是否應該更新,即是否執行 render 函式。

// 元件是否應該更新?- 接受兩個行參,新的屬性、新的狀態
shouldComponentUpdate(nextProps, nextState) {
  if (JSON.stringify(this.state) !== JSON.stringify(nextState)) {
    return true
  } else {
    return false
  }
}

2.5 componentWillReceiveProps

父元件修改屬性觸發,非安全被棄用,同 componentWillMount。處在 diff 中第一個階段,找到哪些需要更新的 Dom。例如:如果父元件連續多次修改屬性傳遞將觸發多次 ajax 請求等。

// 父元件修改屬性觸發,應用在子元件中才有意義
componentWillReceiveProps(nextProps) {
  // 最先獲得父元件傳來的屬性,可以利用屬性進行ajax或者邏輯處理
  // 把屬性轉換為孩子的自己的狀態等
}

3. 銷燬階段

3.1 componentWillUnmount

在刪除元件之前進行清理操作,比如計時器和事件監聽器。

4. 新生命週期

4.1 getDerivedStateFromProps

getDerivedStateFromProps 第一次的初始化元件以及後續的更新過程中(包括自身狀態更新以及父傳子),返回一個物件作為新的 state,返回 null 則說明不需要在這裡更新 state。

在初始化中代替 componentWillMount 。在父傳子中能代替 componentWillReceiveProps。

這裡不能做非同步操作,因為這裡 return 是立即返回的。

static getDerivedStateFromProps(nextProps, nextState) {
  console.log("getDerivedStateFromProps")
  return {

  }
}

4.2 getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 取代了 componentDidUpdate,觸發時間為 update 發生的時候,在 render之後 Dom 渲染之前返回一個值,作為 componentDidUpdate 的第三個引數。

import React, { Component } from 'react'

export default class App extends Component {
  state = {

  }

  // getDerivedStateFromProps 第一次的初始化元件以及後續的更新過程中(包括自身狀態更新以及父傳子),返回一個物件作為新的 state,返回 null 則說明不需要在這裡更新 state
  // 這裡不能做非同步操作,因為這裡 return 是立即返回的
  static getDerivedStateFromProps(nextProps, nextState) {
    console.log("getDerivedStateFromProps")
    return {

    }
  }

  getSnapshotBeforeUpdate() {
    return 100
  }

  componentDidUpdate(prevProps, prevState, value) {
    console.log(value)
  } 

  render() {
    return (
      <div>
        <button onClick={()=>{
          this.setState({

          })
        }}>修改</button>
      </div>
    )
  }
}

5. React 中效能最佳化

5.1 shouldComponentUpdate

React 手動最佳化,控制元件自身或者子元件是否需要更新,尤其在子元件非常多的情況下,需要進行最佳化。

5.2 PureComponent

React 自動最佳化,PureComponent 會幫你比較新 props 跟舊的 props,新的 state 和老的 state(值相等,或者物件含有相同的屬性、且屬性值相等),決定 shouldComponentUpdate 返回 true 或者 false,從而決定要不要呼叫 render function。

注意:如果你的 state 或 props『永遠都會變』,那 PureComponent 並不會比較快,因為 shallowEqual 也需要花時間,比如倒數計時功能,這就不適合使用 Pure Component 了。

5.3 快取技術

React.Component 是使用 ES6 classes 方式定義 React 元件的基類:

class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

PureComponent 和 memo 僅作為效能最佳化的方式而存在。但請不要依賴它來“阻止”渲染,因為這會產生 bug。PureComponnet 和 memo 都是透過對 props 值的淺比較來決定該元件是否需要更新的。

2.1 PureComponent (類元件)

React.PureComponent 與 React.Component 很相似。兩者的區別在於 React.Component 並未實現 shouldComponentUpdate(),而 React.PureComponent 中以淺層對比 props 和 state 的方式來實現了該函式。
如果賦予 React 元件相同的 props 和 state,render() 函式會渲染相同的內容,那麼在某些情況下使用 React.PureComponent 可提高效能。

2.2 memo(函式式元件)

函式元件快取 memo,為啥起 memo 這個名字?在計算機領城,記記化是一種主要用來提高計算機程式速度的最佳化技術方案。它將開銷較大的函式呼叫的返回結果儲存起來,當同樣的輸入再次發生時,則這回快取好的資料,以此提升運算效率。

React.memo 為高階元件。它與 React.PureComponent 非常相似,但只適用於函式元件,而不適用 class 元件。

const MyComponent = function MyComponent(props) {
  /* 使用 props 渲染 */
};
export default React.memo(MyComponent)

如果你的函式元件在給定相同 props 的情況下渲染相同的結果,那麼你可以透過將其包裝在 React.memo 中呼叫,以此透過記憶元件渲染結果的方式來提高元件的效能表現。這意味著在這種情況下,React 將跳過渲染元件的操作並直接複用最近一次渲染的結果,即元件僅在它的 props 發生改變的時候進行重新渲染。通常來說,在元件樹中 React 元件,只要有變化就會走一遍渲染流程。但是 React.memo(),我們可以僅僅讓某些元件進行渲染。

React.memo 僅檢查 props 變更。如果函式元件被 React.memo 包裹,且其實現中擁有 useState 或 useContext 的 Hook,當 context 發生變化時,它仍會重新渲染。

預設情況下其只會對複雜物件做淺層對比,如果你想要控制對比過程,那麼請將自定義的比較函式透過第二個引數傳入來實現。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  
}
export default React.memo(MyComponent, areEqual);

示例:

// 子元件程式碼:
import React, { memo } from 'react';
const Child = ()=>{
	console.log("2. 子元件渲染了")
	return (<div>子元件</div>)
}
export default Child

// 父元件程式碼:
import React, { memo } from 'react';
import Child from './Child.jsx'
const Father = ()=>{
	const [name,setName]=React.useState('');
	console.log("1. 父元件渲染了")
	return (<div>
		/* 在input框中輸入內容,會走setName導致App元件重新渲染,但是子元件Child也會進行渲染。 */
		父元件:<input type="text" value={name} onChange={ev=>setName(ev.target.value)} />
		<Child />
	</div>)
}
// 子元件程式碼:
import React, { memo } from 'react';
const Child = ()=>{
	console.log("2. 子元件渲染了")
	return (<div>子元件</div>)
}
export default memo(Child)
// 父元件程式碼:
import React, { memo } from 'react';
import Child from './Child.jsx'
const Father = ()=>{
	const [name,setName]=React.useState('');
	console.log("1. 父元件渲染了")
	return (<div>
		/* 解決:子元件使用memo包起來 */
		父元件:<input type="text" value={name} onChange={ev=>setName(ev.target.value)} />
		<Child />
	</div>)
}