第五節(jié)——狀態(tài)(State)和生命周期(Lifecycle)

思考一下之前章節(jié)中的時(shí)鐘案例

到目前為止,我們只學(xué)習(xí)了一種更新UI的方式。

我們調(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);

在這一章節(jié)中,我們將學(xué)習(xí)如何把Clock 這個(gè)組件真正地進(jìn)行封裝和復(fù)用。它將會(huì)啟動(dòng)屬于自己的定時(shí)器并且每秒鐘它將會(huì)對(duì)自己進(jìn)行更新。

我們首先開(kāi)始封裝Clock 在頁(yè)面中呈現(xiàn)的樣子:

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);

但是,它缺少很關(guān)鍵的一點(diǎn):事實(shí)上 Clock啟動(dòng)一個(gè)定時(shí)器并且在每秒鐘更新UI應(yīng)該是Clock組件的內(nèi)部實(shí)現(xiàn)。

最理想的情況是我們只這樣寫一次,然后讓Clock組件對(duì)自己進(jìn)行更新:

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

為了可以像這樣實(shí)現(xiàn),我們需要給Clock組件添加狀態(tài)(state)。

state和props相似,但是它是私有的,并且完全被組件控制。

我們?cè)谥疤岬竭^(guò),如果一個(gè)組件以類的方式定義的話,會(huì)有一些額外的特性:局部state變量就是這樣一個(gè)特性:這個(gè)特性僅為類式定義組件提供。

把函數(shù)轉(zhuǎn)變?yōu)轭?/h3>

你可以把一個(gè)函數(shù)式組件通過(guò)以下五個(gè)步驟來(lái)轉(zhuǎn)化為類式組件:
1.以相同的名字創(chuàng)建一個(gè)ES6類,這個(gè)類繼承React.Component
2.在里面添加一個(gè)被稱為render()的方法
3.把函數(shù)體中的內(nèi)容移到render()方法中
4.把render()中的props用this.props替換
5.刪掉原來(lái)的函數(shù)

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

Clock組件現(xiàn)在就是一個(gè)類式定義的組件了,而不再是函數(shù)式定義的組件。

這將使得我們可以使用例如局部(state)和生命周期(Lifecycle)鉤子

在類里添加state局部變量

我們將會(huì)用三步把date從props中移動(dòng)到state中:
1.在render()方法中用this.state.date來(lái)替換this.props.date:

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

2.添加一個(gè)類構(gòu)造器(class constructor)來(lái)標(biāo)識(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傳入到這個(gè)構(gòu)造器的(constructor):

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

類式定義的組件應(yīng)該總是調(diào)用構(gòu)造器函數(shù),并且傳入props

3.從<Clock /> 元素中移除date屬性:

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

我們將會(huì)在稍后給Clock組件自身添加定時(shí)器部分的代碼

現(xiàn)在它看起來(lái)應(yīng)該是這樣的:

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')
);

接下來(lái),我們將使得這個(gè)Clock啟動(dòng)自己的定時(shí)器,并且每秒鐘更新自己一次。

為類添加生命周期函數(shù)

在一個(gè)擁有許多組件的應(yīng)用中,當(dāng)一個(gè)組件被銷毀時(shí),釋放掉它所占用的資源是十分重要的。

我們想要為Clock啟動(dòng)一個(gè)定時(shí)器,無(wú)論何時(shí)它被第一次渲染進(jìn)DOM中。在React中,這個(gè)過(guò)程被稱為掛載(mounting).

我們同時(shí)也想清除一個(gè)定時(shí)器,無(wú)論何時(shí)Clock元素從DOM中移除。在React中,這個(gè)過(guò)程被稱為卸載(unmounting).

我們可以在組件類中聲明一些特殊的方法,在組件掛載(mounting)和卸載(unmounting)的時(shí)候執(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>
    );
  }
}

這些方法被稱為生命周期鉤子(lifecycle hooks)

這個(gè)componentDidMount()生命周期鉤子在組件輸出已經(jīng)被渲染進(jìn)DOM后執(zhí)行,在這里是開(kāi)啟一個(gè)定時(shí)器的好地方:

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

注意,我們?cè)趖his上保存了timer ID

this.props是React自己設(shè)置的,而this.state有著一個(gè)特殊的含義,如果你想存儲(chǔ)一些西并且這些東西不會(huì)為了視覺(jué)上的展示而使用,你可以隨意的在類中手動(dòng)的添加域(fileds)

如果這些東西你不需要在render()方法中使用,那么請(qǐng)不要把它放在state中

我們將會(huì)在componentWillUnmount()生命周期鉤子中清除定時(shí)器:

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

最后,我們將實(shí)現(xiàn)一個(gè)被稱為tick()的函數(shù),來(lái)讓Clock組件每秒鐘運(yùn)行一次

它將會(huì)使用this.setState() 方法來(lái)按照計(jì)劃更新Clock組件中的局部變量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')
);

現(xiàn)在時(shí)鐘每秒鐘滴答走一次啦?。?!

讓我們一起快速的總結(jié)一下發(fā)生了什么,這些方法是按照什么樣的順序來(lái)調(diào)用的:
1.當(dāng)<Clock />元素被傳入到 ReactDOM.render()方法中,React將會(huì)調(diào)用Clock組件中的構(gòu)造器函數(shù)(constructor)。因?yàn)闀r(shí)鐘需要展示當(dāng)前的時(shí)間,它使用一個(gè)包含當(dāng)前時(shí)間的對(duì)象來(lái)初始化this.state。我們將會(huì)在稍后更新這個(gè)state。
2.React然后調(diào)用了Clock組件的render() 方法,通過(guò)這樣使得React知道了在頁(yè)面上應(yīng)該顯示什么。然后React更新DOM來(lái)使之與Clock組件的渲染輸出相匹配。
3.當(dāng)Clock 的渲染輸出被插入的DOM中后,React調(diào)用componentDidMount()生命周期鉤子。在這里面,Clock組件要求瀏覽器開(kāi)啟一個(gè)定時(shí)器,來(lái)每秒鐘調(diào)用一次組件中的tick()方法。
4.每秒鐘瀏覽器調(diào)用一次tick()方法。在它里面,Clock組件按照計(jì)劃更新UI,通過(guò)調(diào)用setState() 方法,向其中傳入一個(gè)包含當(dāng)前時(shí)間的對(duì)象。多虧了setState() 方法的調(diào)用,React知道了state已經(jīng)發(fā)生了改變,然后再一次調(diào)用render()方法去了解應(yīng)該在頁(yè)面上展示什么。這時(shí),在render()方法中的this.state.date發(fā)生了改變,因此渲染輸出將會(huì)包含更新的時(shí)間。React據(jù)此更新DOM。
5.如果Clock組件從DOM中移除,React會(huì)調(diào)用componentWillUnmount()這個(gè)生命周期鉤子來(lái)清除定時(shí)器。

正確地使用state

關(guān)于setState()方法你有三點(diǎn)需要知道:

不要直接對(duì)state進(jìn)行修改

例如,這樣子并不會(huì)讓組件重新渲染:

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

取而代之的,應(yīng)該使用setState()方法:

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

唯一的你可以指定this.state的地方就是在構(gòu)造器函數(shù)中(constructor)中

state更新可能是異步的
React可能處于性能的考慮來(lái)對(duì)多個(gè)setState()方法調(diào)用,通過(guò)批操作只進(jìn)行一整次更新(React may batch multiple setState() calls into a single update for performance.)

由于this.props和this.state的更新可能是異步的,你不應(yīng)該依賴它們的值為了下一次state計(jì)算(you should not rely on their values for calculating the next state.)

例如,這段代碼可能無(wú)法更新這個(gè)counter:

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

為了修復(fù)它,我們將使用第二種形式的setState()方法,它將接收一個(gè)函數(shù)而不是一個(gè)對(duì)象。這個(gè)函數(shù)將會(huì)接收previous state 作為第一個(gè)參數(shù),
更新申請(qǐng)的時(shí)候的props作為第二個(gè)參數(shù)(the props at the time the update is applied as the second argument):

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

在上面我們使用了箭頭函數(shù),我們同樣可以使用常規(guī)的函數(shù):

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

state更新被融合

當(dāng)你調(diào)用setState()方法,React把你提供的對(duì)象融合進(jìn)當(dāng)前的state對(duì)象。

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

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

你可以單獨(dú)的更新它們,通過(guò)分別調(diào)用setState()方法:

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

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

這個(gè)合并是一種淺層的合并,因此this.setState({comments})使得this.state.posts 完好無(wú)損,但是會(huì)把this.state.comments完全覆蓋。

數(shù)據(jù)向下流動(dòng)(The Data Flows Down)

父組件和子組件都不知道一個(gè)確切的組件是有狀態(tài)的(stateful)還是無(wú)狀態(tài)的(stateless),它們不應(yīng)該在意它是用類式定義的還是函數(shù)式定義的。

這也是state被稱為局部的或者封裝的原因,它除了被擁有它的組件訪問(wèn)以外,不可以被任何其它的組件訪問(wèn)。

一個(gè)組件可能會(huì)選擇以props的形式向下傳遞state給它的子組件:

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

它同樣適用于自定義組件:

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

這個(gè)FormattedDate 組件將會(huì)在它的props中接收到date,但是并不知道它是否來(lái)自Clock的state,Clock的props,或者是手動(dòng)輸入的:

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

這通常被稱作自頂向下(top-down)或者 單向(unidirectional)的數(shù)據(jù)流動(dòng)。任何state都被指定的組件所擁有,并且任何源自這個(gè)state的數(shù)據(jù)或者UI僅僅可以影響在組件樹(shù)中位于它們下方的組件(Any state is always owned by some specific component, and any data or UI derived from that state can only affect components “below” them in the tree.)。

如果你把組件樹(shù)想象成props的瀑布流,每個(gè)組件的state在某一點(diǎn)隨意加入的水源,但是它也是向下流動(dòng)的。

為了展示所有的組件都是獨(dú)立的,我們可以創(chuàng)建一個(gè)App組件,它渲染了三個(gè)<Clock/>:

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

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

每一個(gè)Clock啟動(dòng)它自己的定時(shí)器,并且獨(dú)立的進(jìn)行更新。

在React應(yīng)用中,無(wú)論一個(gè)組件是有狀態(tài)的還是無(wú)狀態(tài)的,都被當(dāng)作是組件的細(xì)節(jié)實(shí)現(xiàn)。它們可能隨時(shí)間而改變,你可以在有狀態(tài)的組件;里面使用無(wú)狀態(tài)的組件,反之亦然。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • React版本:15.4.2**翻譯:xiyoki ** 考慮前一節(jié)中滴答作響的時(shí)鐘的例子。到目前為止,我們只學(xué)習(xí)...
    前端xiyoki閱讀 463評(píng)論 0 0
  • 想一下上一節(jié)中那個(gè)滴答計(jì)時(shí)的例子。迄今為止,我們只學(xué)到一種更新UI的方法。我們通過(guò)調(diào)用ReactDOM.rende...
    莫銘閱讀 583評(píng)論 0 0
  • 考慮之前的例子,我們只學(xué)會(huì)了一種方法去更新UI,我們調(diào)用ReactDOM.render()去改變輸出渲染: 在這個(gè)...
    編碼的哲哲閱讀 644評(píng)論 0 0
  • 學(xué)習(xí)使用Clock組件,來(lái)重用和封裝。并設(shè)置定時(shí)器。封裝了一個(gè)定時(shí)器,如: Try it on CodePen. ...
    ZMJun閱讀 510評(píng)論 0 0
  • State 和生命周期 考慮前面章節(jié)中時(shí)鐘的例子。 到目前位置,我們僅學(xué)習(xí)了一種更新 UI 的方式。 我們調(diào)用Re...
    soojade閱讀 1,263評(píng)論 0 1

友情鏈接更多精彩內(nèi)容