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

2022-02-26 14:32 更新

通過(guò)調(diào)用 ReactDOM.render() 來(lái)修改我們想要渲染的元素:

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 上嘗試

如何封裝真正可復(fù)用的 Clock 組件。將設(shè)置自己的計(jì)時(shí)器并每秒更新一次。

我們可以從封裝時(shí)鐘的外觀開(kāi)始:

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 上嘗試

然而,它忽略了一個(gè)關(guān)鍵的技術(shù)細(xì)節(jié):Clock 組件需要設(shè)置一個(gè)計(jì)時(shí)器,并且需要每秒更新 UI。

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

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

我們需要在 Clock 組件中添加 “state” 來(lái)實(shí)現(xiàn)這個(gè)功能。

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

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

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

  1. 創(chuàng)建一個(gè)同名的 ES6 class,并且繼承于 React.Component。
  2. 添加一個(gè)空的 render() 方法。
  3. 將函數(shù)體移動(dòng)到 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ù)。

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

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

向 class 組件中添加局部的 state

我們通過(guò)以下三步將 date 從 props 移動(dòng)到 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. 添加一個(gè) class 構(gòu)造函數(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>
    );
  }
}

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

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

Class 組件應(yīng)該始終使用 props 參數(shù)來(lái)調(diào)用父類的構(gòu)造函數(shù)。

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

我們之后會(huì)將計(jì)時(shí)器相關(guān)的代碼添加到組件中。

代碼如下:

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 上嘗試

接下來(lái),我們會(huì)設(shè)置 Clock 的計(jì)時(shí)器并每秒更新它。

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

在具有許多組件的應(yīng)用程序中,當(dāng)組件被銷毀時(shí)釋放所占用的資源是非常重要的。

當(dāng) Clock 組件第一次被渲染到 DOM 中的時(shí)候,就為其設(shè)置一個(gè)計(jì)時(shí)器。

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

同時(shí),當(dāng) DOM 中 Clock 組件被刪除的時(shí)候,應(yīng)該清除計(jì)時(shí)器。

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

我們可以為 class 組件聲明一些特殊的方法,當(dāng)組件掛載或卸載時(shí)就會(huì)去執(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() 方法會(huì)在組件已經(jīng)被渲染到 DOM 中后運(yùn)行,所以,最好在這里設(shè)置計(jì)時(shí)器:

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

接下來(lái)把計(jì)時(shí)器的 ID 保存在 this 之中(this.timerID)。

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

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

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

最后,我們會(huì)實(shí)現(xiàn)一個(gè)叫 tick() 的方法,Clock 組件每秒都會(huì)調(diào)用它。

使用 this.setState() 來(lái)時(shí)刻更新組件 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)在時(shí)鐘每秒都會(huì)刷新。

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

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

正確地使用 State

關(guān)于 setState() 你應(yīng)該了解三件事:

不要直接修改 State

例如,此代碼不會(huì)重新渲染組件:

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

而是應(yīng)該使用 setState():

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

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

State 的更新可能是異步的

出于性能考慮,React 可能會(huì)把多個(gè) setState() 調(diào)用合并成一個(gè)調(diào)用。

因?yàn)?nbsp;this.props 和 this.state 可能會(huì)異步更新,所以你不要依賴他們的值來(lái)更新下一個(gè)狀態(tài)。

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

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

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

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

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

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

State 的更新會(huì)被合并

當(dāng)你調(diào)用 setState() 的時(shí)候,React 會(huì)把你提供的對(duì)象合并到當(dāng)前的 state。

例如,你的 state 包含幾個(gè)獨(dú)立的變量:

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

然后你可以分別調(diào)用 setState() 來(lái)單獨(dú)地更新它們:

  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ù)據(jù)是向下流動(dòng)的

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

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

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

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

這對(duì)于自定義組件同樣適用:

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

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

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

在 CodePen 上嘗試

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

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

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

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

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

在 CodePen 上嘗試

每個(gè) Clock 組件都會(huì)單獨(dú)設(shè)置它自己的計(jì)時(shí)器并且更新它。

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


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)