React從入門到精通系列之(5)state管理和生命週期鉤子

張亞濤發表於2016-12-13

State和生命週期

考慮前面部分中的滴答時鐘示例(第三章)。
到目前為止,我們只學習了一種更新UI的方法。
我們呼叫ReactDOM.render()來改變渲染輸出:

import React from `react`;
import ReactDOM from `react-dom`;

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

setInterval(tick, 1000);

在本節中,我們將學習如何使Clock元件真正可重用和封裝。 它將設定自己的計時器並每秒更新一次。
我們可以從封裝時鐘的外觀開始:

import React from `react`;
import ReactDOM from `react-dom`;

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

然而,它缺少了一個關鍵要求:時鐘設定一個定時器和每秒更新UI的事實應該是時鐘的實現細節。
理想情況下,我們要寫這一次,並由時鐘本身來更新時間:

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

要實現這一點,我們需要新增“state”到時鐘元件。

state類似於props,但它是私有的,完全由元件控制。

我們之前提到,定義為類元件具有一些附加功能。 內部state就是:一個只有類元件可用的功能。

將函式形式元件改為類形式元件

您可以通過五個步驟將功能元件(如Clock)轉換為類元件 :

  1. 建立一個與擴充套件React.Component相同名稱的ES6類。

  2. 為它新增一個單一的空方法render()

  3. 將函式的主體移動到render()方法中。

  4. render()主體中用this.props替換props

  5. 刪除剩餘的空函式宣告。

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

Clock現在已經重新定義為類元件而不是之前的功能元件了。
這使我們可以使用額外的功能,如內部state和生命週期鉤子。

向類元件中新增state

我們將分為三個步驟把dateprops移動到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>
        );
    }
}
2)新增一個賦值初始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.toLocalTimeString()}.</h2>
            </div>
        );
    }
}

注意我們如何將props傳遞給基類的建構函式:

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

類元件應該總是用props呼叫基類建構函式。

3)從<Clock />元素中刪除date prop:
ReactDOM.render(
    <Clock />,
    document.getElementById(`root`)
);

我們稍後將定時器程式碼新增回元件本身。
結果如下所示:

import React from `react`;
import ReactDOM from `react-dom`;

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

接下來,我們將使時鐘設定自己的定時器,並每秒更新一次。

向類中新增宣告週期方法

在具有許多元件的應用程式中,釋放元件在銷燬時佔用的資源非常重要。
我們想要在第一次將時鐘渲染到DOM時設定一個計時器。 這在React中稱為“安裝(mounting)”
我們還想清除定時器,當時鍾產生的DOM被刪除。 這在React中稱為“解除安裝(unmounting)"
我們可以在元件類上宣告特殊方法,以便在元件裝入和解除安裝時執行一些程式碼:

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.props是由React本身設定的,並且this.state有一個特殊的含義,如果你需要儲存不用於視覺輸出的東西,你可以手動地新增額外的欄位到類中。
如果你不使用render()中的東西,它不應該放置在state中。
我們將拆除componentWillUnmount()生命週期鉤子中的計時器:

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

最後,我們將實現每秒執行的tick()方法。
它將使用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`)
);

現在時鐘每秒鐘都在滴答地走,棒不棒。。。。

讓我們快速回顧一下發生了什麼以及呼叫方法的順序:

  • 1)當將<Clock />傳遞給ReactDOM.render()時,React呼叫Clock元件的建構函式。由於Clock需要顯示當前時間,它使用包括當前時間的物件初始化this.state。我們稍後將更新此state。

  • 2)React然後呼叫Clock元件的render()方法。這是React如何學習應該在螢幕上顯示什麼。 React然後更新DOM以匹配時鐘的渲染輸出。

  • 3)當時鍾輸出插入到DOM中時,React呼叫componentDidMount()生命週期鉤子。在其中,時鐘元件要求瀏覽器設定一個定時器,每秒呼叫tick()一次。

  • 4)每秒鐘瀏覽器呼叫tick()方法。在其中,Clock元件通過呼叫setState()和包含當前時間的物件來排程UI更新。由於setState()呼叫,React知道state已更改,並再次呼叫render()方法來了解螢幕上應該顯示的內容。這個時候,render()方法中的this.state.date將會不同,因此渲染輸出將包括更新的時間。 React相應地更新DOM。

  • 5)如果時鐘元件從DOM中被移除,React將呼叫componentWillUnmount()生命週期鉤子,因此定時器停止。

正確使用state

關於setState()你應該瞭解三件事情:

不要直接修改state

例如,這將不會重新渲染元件:

// 這是錯誤的
this.state.comment = `hello`;

應該使用setState()代替:

// 這是正確的
this.setState({comment: `hello`});

唯一可以分配this.state的地方是建構函式。

state更新可能是非同步的

React可以將多個setState()用批處理為單個更新以實現較高的效能。
因為this.propsthis.state可能是非同步更新的,你不應該依賴它們的值來計算下一個state。
例如,此程式碼可能無法更新計數器:

// 這是錯誤的
this.setState({
    counter: this.state.counter + this.props.increment,
});

要解決它,應該使用回撥函式而不是物件來呼叫setState()。 回撥函式將接收先前的state作為第一個引數,並將應用更新時的props作為第二個引數:

// 這是正確的
this.setState((prevState, props) => ({
    counter: prevState.counter + props.increment
}));

我們使用上面的箭頭函式,但它也可以與常規函式一起使用:

// 這同樣也是正確的,將剪頭函式改為普通函式
this.setState(function(prevState, props) {
   return {
       counter: prevState.counter + prps.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而已。

資料是向下流動的

父元件和子元件都不能知道某個元件是有State的還是無State的,並且它們不應該關心它是否為功能元件或類元件。

這就是為什麼State通常被設定為區域性變數或封裝到元件內部。 除了擁有和設定它的元件之外的其他任何元件都不能訪問它。

元件可以選擇將其state作為props傳遞給其子元件:

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

這也適用於使用者定義的元件:

<formattedDate date={this.state.data} />

FormattedDate元件將在其props中接收date,並且不知道它是來自時鐘的stateprops還是手動輸入

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

這通常被稱為“自頂向下”“單向”資料流。 任何state總是由一些特定元件擁有,並且從該state派生的任何資料或UI只能影響樹中的“下面”元件。

如果你想象一個元件樹作為props的瀑布流,每個元件的state就像一個額外的水源,它可以在任意點連線它,但也向下流。

為了顯示所有元件都是真正隔離的,我們可以建立一個App元件來渲染三個<Clock>

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

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

每個時鐘設定自己的定時器並獨立更新。
在React應用程式中,元件是有狀態還是無狀態被視為可能隨時間更改的元件的實現細節。 您可以在有狀態元件內使用無狀態元件,反之亦然。

相關文章