React 狀態管理:狀態與生命週期

知識小集發表於2018-06-20

在《React 狀態管理:從 Props 和 State 說起》一篇中,我們介紹了一個 React 元件狀態管理的基礎:props 和 state,以及這兩個物件對 React 元件渲染的影響。在這一篇裡,我們來看看這兩個物件在元件的生命週期中,是如何與元件相互作用的。

生命週期

生命週期是一個很寬泛的概念。小到一個物件,大到一個應用,都有生命週期。而對於 UI 層面的開發來說,不管是 iOS 中 View Controller ,Android 中的 Activity/Fragment,還是 React/Vue 中的 UI 元件,也都有涉及到生命週期。這些物件的生命週期除了建立、銷燬外,可能還涉及到一系列的操作,如載入、顯示、更新、解除安裝等。

在 React 中,元件生命週期的維護一般是由框架負責的,包括元件的建立、銷燬、變更等操作,我們一般不需要直接去手動去做處理。不過,框架同時也會提供一些 hook 方法,讓我們在元件生命週期的某些特定時間點,可以加入我們自己的操作。

對於 React 元件來說,生命週期主要包含三個階段:建立(掛載)過程、銷燬(解除安裝)過程和存在期,每個階段都有相應的 hook 方法,我們可以用一張圖來概括:

React 狀態管理:狀態與生命週期

我們在這不詳細介紹每一個方法,而是主要介紹 props 和 state 兩個物件是如何參與整個元件的生命週期的。

元件的生命週期相關可參考《深入React技術棧》

render() 方法

在這裡需要重提一下 render() 方法。render() 方法是一個 class 元件必須實現的方法。props 和 state 物件中的值的改變會引起元件的重新渲染,從而會呼叫元件的 render() 方法。在這個方法中,我們可以從 props 和 state 中取出值來使用,以確定元件如何去渲染。

需要注意的是,render() 必須是一個純函式,不能在這裡面修改元件的狀態或執行有副作用的操作。

Props 與生命週期

props 物件定義了元件的屬性,我們在父元件中建立某個元件的例項時,可以通過屬性向這個元件傳遞值。父元件通過屬性傳入的值,都會以 key-value 的形式儲存在元件的 props 物件中。

宣告 Props 的預設值

首先,我們可以為元件的屬性設定預設值,這樣父元件建立元件時,不需要設定所有的屬性值。此時,元件內部使用 props 中的值時,如果父元件沒有設定,則會使用預設值。

在 ES6 的類中,我們可以通過以下方式來設定預設值:

class Greeting extends React.Component {
  // ...
}

Greeting.defaultProps = {
  name: 'Mary'
};
複製程式碼

這裡需要注意的是,defaultProps 是 Greeting 元件本身的屬性(類屬性),而不是元件例項的屬性,這也就意味著在整個應用中,defaultProps 的值只會設定一次,而且是在例項化元件之前。讓我們更直觀地來看看這個流程:

class Component extends React.Component {
  render() {
    return React.createElement('span');
  }
}

Component.defaultProps = {fruit: 'persimmon'};

const container = document.createElement('div');
const instance = ReactDOM.render(
   React.createElement(Component, {fruit: 'mango'}),
   container,
);
複製程式碼

在通過 React.createElement() 建立元件例項之前,defaultProps 就已經設定好了。這就涉及到另一個問題,我們可以使用 static 語法糖在 class 內部來設定 defaultProps,此時我們不能在 defaultProps 中使用 this 去引用例項的屬性(相信有物件導向語言基礎的童鞋都知道問題所在)。

class Component extends React.Component {
  static defaultProps = {
  	 fruit: 'persimmon',
  	 // value: this.***,			// error
  }
  render() {
    return React.createElement('span');
  }
}
複製程式碼

修改 Props

修改 Props 物件中某個屬性值,會引發元件的重新渲染。在這一過程中,會呼叫一系列的 hook 方法,如上圖中所示。這個流程 state 的變更是差不多的。只是 props 的流程多了一個方法 componentWillReceiveProps()。實際上,在當前版本的 React 中,這個方法已被重新命名為 UNSAFE_componentWillReceiveProps(nextProps),其引數 nextProps 包含修改後的新的屬性值。該方法會在元件接收到新的屬性值之前呼叫。

在這個方法中,我們可以對比修改前後的 props 的值,來做相應的處理,比如呼叫 setState() 方法來設定 state。不過官方顯然不建議我們這樣做,而在建議我們在 componentDidMount() 方法中來處理。

另外,如果父元件的行為導致元件重新渲染,也會觸發這個方法執行,即使元件的屬性值並沒有改變。

componentWillReceiveProps() 之後的流程與 state 更改的流程一致,所以我們統一在下面來討論這個過程。

State 與生命週期

state 是元件的內生狀態。這讓元件看上去像是一個狀態機,在不同的狀態下,顯示不同的內容。

state 的初始化

state 是與每個元件例項自身相關的,我們通常在 constructor() 中,通過 this.state 來設定其初始值:

constructor(props) {
  super(props);
  // Don't call this.setState() here!
  this.state = { counter: 0 };
}
複製程式碼

這裡有幾點需要注意:

  1. 在 constructor() 構造器中必須首先呼叫 super(props),否則在構造器中 this.props 是未定義的,可能會引發 bug;
  2. constructor() 是唯一直接設定 this.state 的地方,其它地方都通過 setState() 方法來設定 state;
  3. 在 constructor() 中不能使用 setState() 方法來設定 state;
  4. 儘量避免將 props 的值拷貝到 state 中,一方面是沒必要,另一方面是 props 修改時,對應 state 的值並不會同步修改(反模式);

修改 state

state 值的修改同樣會引發元件的重新渲染,這個過程中呼叫的 hook 方法和 props 的變更一樣。在這個過程中,會呼叫以下幾個方法:

  1. shouldComponentUpdate(nextProps, nextState)
  2. componentWillUpdate(nextProps, nextState)
  3. render()
  4. componentDidUpdate(prevProps, prevState, snapshot)

shouldComponentUpdate(nextProps, nextState) 方法中,我們可以根據 nextProps 或 nextState 的值來確定是否需要重新渲染元件。預設是返回 true,如果返回 false,則後續流程會被中斷。不過,這個方法的用途更多的是在於效能的優化,以減少不必要的渲染,而不是去依賴它來“阻止”渲染。

componentWillUpdate(nextProps, nextState) 是在即將更新元件之前的操作。這個方法在當前最新版本中同樣被重新命名為 UNSAFE_componentWillUpdate(nextProps, nextState)。在這個方法中,我們不能去呼叫 setState 方法,或者做其它可能會引起元件重新渲染的操作。

componentDidUpdate(prevProps, prevState, snapshot) 方法會在元件重新渲染完成之後呼叫。我們可以看到這時引數名已變成 prevPropsprevState,儲存的是更新之前的 props 和 state。在這個方法中,可以去執行 DOM 操作,也可以去做網路請求等操作。同樣,在這個方法中可以呼叫 setState() 方法,不過一定需要有一個條件語句來控制 setState() 方法的呼叫,否則會導致無限迴圈。

被遺棄的 hook 方法

實際上,在官方的最新文件中,有幾個生命週期方法已被標記為 UNSAFE

  • UNSAFE_componentWillMount()
  • UNSAFE_componentWillUpdate()
  • UNSAFE_componentWillReceiveProps()

這幾個方法對應的無字首方法如下:

  • componentWillMount()
  • componentWillUpdate()
  • componentWillReceiveProps()

這幾個方法在 v17 之前還會繼續存在。不建議使用這幾個方法是因為在這幾個方法中做一些 side effect 時(如 setState() )時,會引發一些意想不到的問題。

在省去這幾個方法後,生命週期的流程圖就變成如下:

React 狀態管理:狀態與生命週期

實際上在這個流程中我們可以看到下面這個方法:

static getDerivedStateFromProps(props, state)
複製程式碼

這是在 React 16.3 新加入的方法,主要是為了避免在 UNSAFE_componentWillReceiveProps 中使用 setState() 而產生的副作用。關於這個方法,需要注意幾點:

  • 這個方法在每次 render() 之前都會呼叫;
  • 這是一個 static 方法,即類方法;
  • 它返回一個物件來更新 state,或者返回 null,什麼也不做

小結

這裡我們主要介紹了 props 和 state 與元件生命週期的關聯。需要知道的就是這兩個物件中的值發生改變時會引發元件的重新渲染,然後會執行一些 hook 方法。特別需要注意的是在這些 hook 方法中,我們需要正確的使用 setState() 方法,否則會導致一些未知的問題。

有了這些知識點,再加上元件事件處理相關的內容,就可以做一些最基本的狀態管理了。不過,如果直接在元件中管理所有狀態,可能會造成一些問題:

  • 元件需要處理很多事情,如網路請求、本地快取等,這樣元件的程式碼會很多很亂,這是我們不願意看到的;
  • 元件中產生副作用的操作過多,不利於元件的複用;

為此,我們需要想一些辦法來解決這些問題。這就需要引入一起框架,如 Flux,Redux 等。下一篇,我們先來講講 Flux。

參考

  1. 官方文件:React.Component

知識小集是一個團隊公眾號,主要定位在移動開發領域,分享移動開發技術,包括 iOS、Android、小程式、移動前端、React Native、weex 等。每週都會有 原創 文章分享,我們的文章都會在公眾號首發。歡迎關注檢視更多內容。

React 狀態管理:狀態與生命週期

相關文章