React State(狀態(tài))& 生命周期

2022-02-26 14:32 更新

通過調用 ReactDOM.render() 來修改我們想要渲染的元素:

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(element,document.getElementById('root'));
}

setInterval(tick, 1000);

在 CodePen 上嘗試

如何封裝真正可復用的 Clock 組件。將設置自己的計時器并每秒更新一次。

我們可以從封裝時鐘的外觀開始:

function Clock(props) {
  return (
    <div>      
        <h1>Hello, world!</h1>      
        <h2>It is {props.date.toLocaleTimeString()}.</h2>    
    </div>  
    );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,    
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

在 CodePen 上嘗試

然而,它忽略了一個關鍵的技術細節(jié):Clock 組件需要設置一個計時器,并且需要每秒更新 UI。

理想情況下,我們希望只編寫一次代碼,便可以讓 Clock 組件自我更新:

ReactDOM.render(
    <Clock />,  
    document.getElementById('root')
);

我們需要在 Clock 組件中添加 “state” 來實現(xiàn)這個功能。

State 與 props 類似,但是 state 是私有的,并且完全受控于當前組件。

將函數(shù)組件轉換成 class 組件

通過以下五步將 Clock 的函數(shù)組件轉成 class 組件:

  1. 創(chuàng)建一個同名的 ES6 class,并且繼承于 React.Component。
  2. 添加一個空的 render() 方法。
  3. 將函數(shù)體移動到 render() 方法之中。
  4. 在 render() 方法中使用 this.props 替換 props。
  5. 刪除剩余的空函數(shù)聲明。
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

在 CodePen 上嘗試

現(xiàn)在 Clock 組件被定義為 class,而不是函數(shù)。

每次組件更新時 render 方法都會被調用,但只要在相同的 DOM 節(jié)點中渲染 <Clock /> ,就僅有一個 Clock 組件的 class 實例被創(chuàng)建使用。

這就使得我們可以使用如 state 或生命周期方法等很多其他特性。

向 class 組件中添加局部的 state

我們通過以下三步將 date 從 props 移動到 state 中:

  1. 把 render() 方法中的 this.props.date 替換成 this.state.date :
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      
        </div>
    );
  }
}
  1. 添加一個 class 構造函數(shù),然后在該函數(shù)中為 this.state 賦初值:
class Clock extends React.Component {
    constructor(props) {
        super(props);
        this.state = {date: new Date()};  
    }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

通過以下方式將 props 傳遞到父類的構造函數(shù)中:

  constructor(props) {
    super(props);    
    this.state = {date: new Date()};
  }

Class 組件應該始終使用 props 參數(shù)來調用父類的構造函數(shù)。

  1. 移除 <Clock /> 元素中的 date 屬性:
ReactDOM.render(
    <Clock />,  
    document.getElementById('root')
);

我們之后會將計時器相關的代碼添加到組件中。

代碼如下:

class Clock extends React.Component {
    constructor(props) {    
        super(props);    
        this.state = {date: new Date()};  
    }
    render() {
        return (
            <div>
            <h1>Hello, world!</h1>
            <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      
            </div>
        );
    }
}

ReactDOM.render(
  <Clock />,  document.getElementById('root')
);

在 CodePen 上嘗試

接下來,我們會設置 Clock 的計時器并每秒更新它。

將生命周期方法添加到 Class 中

在具有許多組件的應用程序中,當組件被銷毀時釋放所占用的資源是非常重要的。

當 Clock 組件第一次被渲染到 DOM 中的時候,就為其設置一個計時器。

這在 React 中被稱為“掛載(mount)”。

同時,當 DOM 中 Clock 組件被刪除的時候,應該清除計時器。

這在 React 中被稱為“卸載(unmount)”。

我們可以為 class 組件聲明一些特殊的方法,當組件掛載或卸載時就會去執(zhí)行這些方法:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {  }
  componentWillUnmount() {  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

這些方法叫做“生命周期方法”。

componentDidMount() 方法會在組件已經被渲染到 DOM 中后運行,所以,最好在這里設置計時器:

componentDidMount() {
    this.timerID = setInterval(() => this.tick(),1000);  
}

接下來把計時器的 ID 保存在 this 之中(this.timerID)。

盡管 this.props 和 this.state 是 React 本身設置的,且都擁有特殊的含義,但是其實你可以向 class 中隨意添加不參與數(shù)據流(比如計時器 ID)的額外字段。

我們會在 componentWillUnmount() 生命周期方法中清除計時器:

componentWillUnmount() {
    clearInterval(this.timerID);  
}

最后,我們會實現(xiàn)一個叫 tick() 的方法,Clock 組件每秒都會調用它。

使用 this.setState() 來時刻更新組件 state:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({date: new Date()});
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

在 CodePen 上嘗試

現(xiàn)在時鐘每秒都會刷新。

讓我們來快速概括一下發(fā)生了什么和這些方法的調用順序:

  1. 當 <Clock /> 被傳給 ReactDOM.render()的時候,React 會調用 Clock 組件的構造函數(shù)。因為 Clock 需要顯示當前的時間,所以它會用一個包含當前時間的對象來初始化 this.state。我們會在之后更新 state。
  2. 之后 React 會調用組件的 render() 方法。這就是 React 確定該在頁面上展示什么的方式。然后 React 更新 DOM 來匹配 Clock 渲染的輸出。
  3. 當 Clock 的輸出被插入到 DOM 中后,React 就會調用 ComponentDidMount() 生命周期方法。在這個方法中,Clock 組件向瀏覽器請求設置一個計時器來每秒調用一次組件的 tick() 方法。
  4. 瀏覽器每秒都會調用一次 tick() 方法。 在這方法之中,Clock 組件會通過調用 setState() 來計劃進行一次 UI 更新。得益于 setState() 的調用,React 能夠知道 state 已經改變了,然后會重新調用 render() 方法來確定頁面上該顯示什么。這一次,render() 方法中的 this.state.date 就不一樣了,如此以來就會渲染輸出更新過的時間。React 也會相應的更新 DOM。
  5. 一旦 Clock 組件從 DOM 中被移除,React 就會調用 componentWillUnmount() 生命周期方法,這樣計時器就停止了。

正確地使用 State

關于 setState() 你應該了解三件事:

不要直接修改 State

例如,此代碼不會重新渲染組件:

// Wrong
this.state.comment = 'Hello';

而是應該使用 setState():

// Correct
this.setState({comment: 'Hello'});

構造函數(shù)是唯一可以給 this.state 賦值的地方:

State 的更新可能是異步的

出于性能考慮,React 可能會把多個 setState() 調用合并成一個調用。

因為 this.props 和 this.state 可能會異步更新,所以你不要依賴他們的值來更新下一個狀態(tài)。

例如,此代碼可能會無法更新計數(shù)器:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

要解決這個問題,可以讓 setState() 接收一個函數(shù)而不是一個對象。這個函數(shù)用上一個 state 作為第一個參數(shù),將此次更新被應用時的 props 做為第二個參數(shù):

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

上面使用了箭頭函數(shù),不過使用普通的函數(shù)也同樣可以:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

State 的更新會被合并

當你調用 setState() 的時候,React 會把你提供的對象合并到當前的 state。

例如,你的 state 包含幾個獨立的變量:

constructor(props) {
    super(props);
    this.state = {
        posts: [],      
        comments: []    
    };
}

然后你可以分別調用 setState() 來單獨地更新它們:

  componentDidMount() {
    fetchPosts().then(response => {
        this.setState({
            posts: response.posts      
        });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments      });
    });
  }

這里的合并是淺合并,所以 this.setState({comments}) 完整保留了 this.state.posts, 但是完全替換了 this.state.comments。

數(shù)據是向下流動的

不管是父組件或是子組件都無法知道某個組件是有狀態(tài)的還是無狀態(tài)的,并且它們也并不關心它是函數(shù)組件還是 class 組件。

這就是為什么稱 state 為局部的或是封裝的的原因。除了擁有并設置了它的組件,其他組件都無法訪問。

組件可以選擇把它的 state 作為 props 向下傳遞到它的子組件中:

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

這對于自定義組件同樣適用:

<FormattedDate date={this.state.date} />

FormattedDate 組件會在其 props 中接收參數(shù) date,但是組件本身無法知道它是來自于 Clock 的 state,或是 Clock 的 props,還是手動輸入的:

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

在 CodePen 上嘗試

這通常會被叫做“自上而下”或是“單向”的數(shù)據流。任何的 state 總是所屬于特定的組件,而且從該 state 派生的任何數(shù)據或 UI 只能影響樹中“低于”它們的組件。

如果你把一個以組件構成的樹想象成一個 props 的數(shù)據瀑布的話,那么每一個組件的 state 就像是在任意一點上給瀑布增加額外的水源,但是它只能向下流動。

為了證明每個組件都是真正獨立的,我們可以創(chuàng)建一個渲染三個 Clock 的 App 組件:

function App() {
  return (
    <div>
        <Clock />      
        <Clock />      
        <Clock />    
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

在 CodePen 上嘗試

每個 Clock 組件都會單獨設置它自己的計時器并且更新它。

在 React 應用中,組件是有狀態(tài)組件還是無狀態(tài)組件屬于組件實現(xiàn)的細節(jié),它可能會隨著時間的推移而改變。你可以在有狀態(tài)的組件中使用無狀態(tài)的組件,反之亦然。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號