iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 7
1
自我挑戰組

與 React 交朋友的三十天學習之旅系列 第 7

Class-based Component 的生命週期

今天要學習關於 Class-based Component 的生命週期(lifecycle),了解元件(Component)在建立、更新及銷毀時會經歷過哪些階段、元件和子元件之間的建立順序與我們可以在這些時候做些什麼事情。

而這些生命週期的方法只能在Class-based Component 中使用,在 Function Component 中則是需要透過 React hooks 的方式達成,但這個我們留待後面的篇幅在做學習。

而 Class-based Component 的生命週期會拆分兩篇來做學習,今天先透過測試範例,一步步了解 Class-based Component 中建立元件、更新元件以及銷毀元件時會用到的生命週期方法:

明天的部分則是針對這些方法來了解官方的定義。

接著讓我們趕緊進入正題吧!

Class component 的生命週期

在官方文件中有提供一份清楚易懂的生命週期示意圖

接著,讓我們先透過幾個簡單的測試範例來了解關於生命週期,後續再來看看官方對於每個方法又是怎麼定義的吧!!

相關測試範例,點擊前往

首先我們建立 Card 元件,並在 App 中將 state 的中 persons 的資料 props 到 Card 元件:

// App.js
import React, { Component } from 'react';
import './App.css';
import Card from './Card';

class App extends Component {
  constructor() {
    super();
    this.state = {
      persons: [
        { name: 'Bill', age: 27, habbit: 'Play ball'}
      ]
    }
  }
  render() {
    return (
      <div className="App">
        <Card persons={ this.state.persons }/>
      </div>
    );
  }
}

export default App;
// Card.js

import React, { Component } from 'react';
import './index.css';

class Card extends Component {
  render() {
    return (
      <div className="card">
        <h1>{ this.props.persons[0].name }</h1>
        <p>{ this.props.persons[0].age }</p>
        <p>{ this.props.persons[0].habbit }</p>
      </div>
    );
  }
}

export default Card;

接著在 App 這個 containers 與 Card 這個元件中,我們依據官方提供的示意圖,設定好生命週期的方法。

這邊我們先觀察 Mounting 時的階段,依如下順序在 App, Card 中設定:

  • constructor
  • static getDerivedStateFromProps
  • render
  • componentDidMount
// App.js Mounting life cycle
import React, { Component } from 'react';
import './App.css';
import Card from './Card';

class App extends Component {
  constructor() {
    console.log('[App.js]: constructor');
    super();
    this.state = {
      persons: [
        { name: 'Bill', age: 27, habbit: 'Play ball'}
      ],
    }
  }
  static getDerivedStateFromProps(props, state) {
    console.log('----------------');
    console.log('[App.js]: getDerivedStateFromProps');
    console.log('props', props);
    console.log('state', state);
    console.log('----------------');
    return state;
  }

  componentDidMount() {
    console.log('[App.js]: componentDidMount');
  }
  render() {
    console.log('[App.js]: render');
    return (
      <div className="App">
        <Card persons={ this.state.persons }/>
      </div>
    );
  }
}

export default App;
// Card.js Mounting life cycle
import React, { Component } from 'react';
import './index.css';

class Card extends Component {
  constructor() {
    super();
    console.log('[Card.js]: constructor');
  }

  static getDerivedStateFromProps(props, state) {
    console.log('----------------');
    console.log('[Card.js]: getDerivedStateFromProps');
    console.log('props', props);
    console.log('state', state);
    console.log('----------------');
    return null;
  }

  componentDidMount() {
    console.log('[Card.js]: componentDidMount');
  }

  render() {
    console.log('[Card.js]: render');
    return (
      <div className="card">
        <h1>{ this.props.persons[0].name }</h1>
        <p>{ this.props.persons[0].age }</p>
        <p>{ this.props.persons[0].habbit }</p>
      </div>
    );
  }
}

export default Card;

此時會得到這樣的結果:

而其中在 Card 元件中使用 getDerivedStateFromProps 方法會報錯的部分是因為 Card 元件這邊是接收 App props 進來的值,而本身沒有 init state,所以才會報錯,而稍後我們會了解 getDerivedStateFromProps 這個方法的定義。

不過從結果可以得知,==當 App containers 中有子元件 Card 時,在 App 執行到 render 方法時,並不會先完成 App 的掛載(componentDidMount),而是會進入到 Card 中並依據這個元件的生命週期建立後,最後才會回到 App 並完成掛載(componentDidMount)==。

接著讓我們往 Updating 的階段繼續前進,這邊我們設定一個情境:

點擊卡片時會將名字從「Bill」切換成 「Henry」。

並將如下的生命週期方法依序設定:

  • static getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

相關測試範例: 點擊前往

// App.js updating life cycle
import React, { Component } from 'react';
import './App.css';
import Card from './Card';


class App extends Component {
  constructor() {
    super();
    this.state = {
      persons: [
        { name: 'Bill', age: 27, habbit: 'Play ball'}
      ],
    }
  }

  changeNameHandler = () => {
    this.setState(state => {
      const arr = state.persons;
      arr[0].name = 'Henry';
      return {
        persons: arr
      }
    });
  }

  static getDerivedStateFromProps(props, state) {
    console.log('----------------');
    console.log('[App.js]: getDerivedStateFromProps');
    console.log("props", props);
    console.log("state", state);
    console.log('----------------');
    return state;
  }

  shouldComponentUpdate() {
    console.log('[App.js]: shouldComponentUpdate');
    return true;
  }

  getSnapshotBeforeUpdate(props, state) {
    console.log('----------------');
    console.log('[App.js]: getSnapshotBeforeUpdate');
    console.log("props", props);
    console.log("state", state);
    console.log('----------------');
    return null;
  }

  componentDidUpdate() {
    console.log('[App.js]: componentDidUpdate'); 
  }

  render() {
    console.log('[App.js]: render');
    return (
      <div className="App">
        <Card 
          persons={ this.state.persons } 
          changeNameHandler={ this.changeNameHandler }
        />
      </div>
    );
  }
}

export default App;
// Card.js updating life cycle
import React, { Component } from 'react';
import './index.css';

class Card extends Component {
  constructor() {
    super();
  }

  static getDerivedStateFromProps(props, state) {
    console.log('----------------');
    console.log('[Card.js]: getDerivedStateFromProps');
    console.log("props", props);
    console.log("state", state);
    console.log('----------------');
    return null;
  }

  shouldComponentUpdate() {
    console.log('[Card.js]: shouldComponentUpdate');
    return true;
  }

  getSnapshotBeforeUpdate(props, state) {
    console.log('----------------');
    console.log('[Card.js]: getSnapshotBeforeUpdate');
    console.log("props", props);
    console.log("state", state);
    console.log('----------------');
    return null;
  }

  componentDidUpdate() {
    console.log('[Card.js]: componentDidUpdate'); 
  }
  render() {
    console.log('[Card.js]: render');
    return (
      <div className="card" onClick={ this.props.changeNameHandler }>
        <h1>{ this.props.persons[0].name }</h1>
        <p>{ this.props.persons[0].age }</p>
        <p>{ this.props.persons[0].habbit }</p>
      </div>
    );
  }
}

export default Card;

可以取得以下結果:

==這邊比較特別的大概是執行 getSnapshotBeforeUpdate 的部分,並不是先完成 Card 元件更新的生命週期方法後回到 App 中,而是先回到 App 中執行完 getSnapshotBeforeUpdate 方法後才會完成 Card 元件的更新,接著才是 App 的更新==。

最後一個階段是 Unmounting,這個階段會在 component 被 unmount 或者 destroy 的時候觸發,這邊我們一樣給一個情境:

點擊按鈕時,會將卡片隱藏起來

相關測試範例: 點擊前往

// App.js unmounting life cycle
import React, { Component } from 'react';
import './App.css';
import Card from './Card';


class App extends Component {
  constructor() {
    super();
    this.state = {
      persons: [
        { name: 'Bill', age: 27, habbit: 'Play ball'}
      ],
      isHide: false
    }
  }

  changeNameHandler = () => {
    this.setState(state => {
      const arr = state.persons;
      arr[0].name = 'Henry';
      return {
        persons: arr
      }
    });
  }

  hideCardHandler = () => {
    this.setState({ persons: [] });
  }

  static getDerivedStateFromProps(props, state) {
    console.log('----------------');
    console.log('[App.js]: getDerivedStateFromProps');
    console.log("props", props);
    console.log("state", state);
    console.log('----------------');
    return state;
  }

  shouldComponentUpdate() {
    console.log('[App.js]: shouldComponentUpdate');
    return true;
  }

  getSnapshotBeforeUpdate(props, state) {
    console.log('----------------');
    console.log("props", props);
    console.log("state", state);
    console.log('----------------');
    return null;
  }

  componentDidUpdate() {
    console.log('[App.js]: componentDidUpdate'); 
  }

  componentWillUnmount() {
    console.log('[App.js]: componentWillUnmount');
  }

  render() {
    console.log('[App.js]: render');
    return (
      <div className="App">
        { this.state.persons.map(person => (
          <Card 
            persons={ this.state.persons } 
            changeNameHandler={ this.changeNameHandler }
            isHide={ this.state.isHide }
          /> 
        ))}
        <button onClick={ this.hideCardHandler }>隱藏卡片</button>
      </div>
    );
  }
}

export default App;
// Card.js unmounting life cycle
import React, { Component } from 'react';
import './index.css';

class Card extends Component {
  constructor() {
    super();
  }

  static getDerivedStateFromProps(props, state) {
    console.log('----------------');
    console.log('[Card.js]: getDerivedStateFromProps');
    console.log(props);
    console.log(state);
    console.log('----------------');
    return null;
  }

  shouldComponentUpdate() {
    console.log('[Card.js]: shouldComponentUpdate');
    return true;
  }

  getSnapshotBeforeUpdate(props, state) {
    console.log('----------------');
    console.log('[Card.js]: getSnapshotBeforeUpdate');
    console.log(props);
    console.log(state);;
    console.log('----------------');
    return null;
  }

  componentDidUpdate() {
    console.log('[Card.js]: componentDidUpdate'); 
  }

  componentWillUnmount() {
    console.log('[Card.js]: componentWillUnmount');
  }

  render() {
    console.log('[Card.js]: render');

    return (
      <div className="card" onClick={ this.props.changeNameHandler }>
        <h1>{ this.props.persons[0].name }</h1>
        <p>{ this.props.persons[0].age }</p>
        <p>{ this.props.persons[0].habbit }</p>
      </div>
    );
  }
}

export default Card;

可以得到執行結果如下:

由於被 destroy 的是 Card 元件,所以可以發現在 App 中的 life cycle 一樣會執行,但是 Card 元件則是只有觸發了 ComponentWillUnmount 方法。

以上就是關於 Class-based component 中從建立元件、更新元件以及銷毀元件的生命週期展示,下個篇幅就針對上述的生命週期方法,看看官方是怎麼定義的吧!

鐵人賽文章與程式碼同步發佈於:

  1. 個人部落格
  2. Github

資源


上一篇
事件處理的使用與了解
下一篇
Class-based Component 生命週期方法(續)
系列文
與 React 交朋友的三十天學習之旅30

1 則留言

0
tsuifei
iT邦新手 5 級 ‧ 2020-10-03 03:13:30

React 的生命週期表似乎比 Vue 的單純哩?

我要留言

立即登入留言