React專題:可變狀態

本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出
來個人 GitHub repo 閱讀完整的專題文章
來個人 我的博客 得到無與倫比的閱讀體驗

React使用一個特殊的對象this.state來管理組件內部的狀態。javascript

而後開發者就能夠經過描述狀態來控制UI的表達。java

如何描述狀態呢?react

通常咱們會在constructor生命週期鉤子初始化狀態。git

import React, { Component } from 'react';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = { name: '', star: 0 };
    }
}

export default App;

也能夠直接用屬性初始化器的寫法,看起來更加簡潔。github

而後經過this.setSatate()來改變狀態。算法

import React, { Component } from 'react';

class App extends Component {
    state = { name: '', star: 0 };
    
    componentDidMount() {
        this.setState({ name: 'react', star: 1 });
    }
}

export default App;

this.state

首先,改變狀態有特殊的門路

開發者不能直接改變this.state的屬性,而是要經過this.setSatate方法。json

爲何要這樣設計?api

多是爲了更加語義化吧,開發者清楚本身在更新狀態,而不是像Vue那樣改變於無形。異步

不過別急,我爲正在閱讀的你準備了一個炸彈:ide

猜猜下面例子最終渲染出來的star是多少?

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        this.state.star = 1000;
        this.setState(prevState => ({ star: prevState.star + 1 }));
    }
    
    // componentDidMount() {
        // this.setState(prevState => ({ star: prevState.star + 1 }));
        // this.state.star = 1000;
    // }
    
    // componentDidMount() {
        // this.state.star = 1000;
        // this.setState({ star: this.state.star + 1 });
    // }
    
    // componentDidMount() {
        // this.setState({ star: this.state.star + 1 });
        // this.state.star = 1000;
    // }
}

export default App;

答案是1001。

誒,不是說不能直接改變this.state的屬性麼?

聽我講,首先,this.state並非一個不可變對象,你(非得較勁的話)是能夠直接改變它的屬性的。可是它不會觸發render生命週期鉤子,也就不會渲染到UI上。

不過,既然你確實改變了它的值,若是以後調用了this.setSatate()的話,它會在你直接改變的值的基礎上再作更新。

因此呀少年,要想不懵逼,得靠咱們本身的代碼規範。

至於註釋的部分,只是爲了說明順序問題。

第一部分註釋渲染出來的star是1001。由於回調會首先計算star的值,而這時候star的值是1000。

第二部分註釋渲染出來的star是1001。這很好理解。

第三部分註釋渲染出來的star是1。這也好理解,這個時候star的值仍是0。

其次,狀態更新會合並處理

你們也看到了,咱們能夠每次更新部分狀態。

新狀態並不會覆蓋舊狀態,而是將已有的屬性進行合併操做。若是舊狀態沒有該屬性,則新建。

這相似於Object.assign操做。

並且合併是淺合併。

只有第一層的屬性纔會合併,更深層的屬性都會覆蓋。

import React, { Component } from 'react';

class App extends Component {
    state = { userInfo: { name: '', age: 0 } };
    
    componentDidMount() {
        this.setState({ userInfo: { age: 13 } });
    }
}

export default App;

最後,能夠有不是狀態的狀態

若是你須要存儲某種狀態,可是不但願在狀態更新的時候觸發render生命週期鉤子,那麼徹底能夠直接存儲到實例的屬性上,只要不是this.state的屬性。使用起來仍是很自由的。

異步更新

什麼叫異步更新?

異步更新說的直白點就是批量更新。

它不是真正的異步,只是React有意識的將狀態攢在一塊兒批量更新。

React組件有本身的生命週期,在某兩個生命週期節點之間作的全部的狀態更新,React會將它們合併,而不是當即觸發UI渲染,直到某個節點纔會將它們合併的值批量更新。

如下,組件更新以後this.state.star的值是1。

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        this.setState({ star: this.state.star + 1 });
        this.setState({ star: this.state.star + 1 });
        this.setState({ star: this.state.star + 1 });
    }
}

export default App;

由於這些狀態改變的操做都是在組件掛載以後、組件更新以前,因此實際上它們並無當即生效。

this.state.star的值一直是0,儘管狀態被屢次操做,它獲得的值一直是1,所以合併以後this.state.star的仍是1,並非咱們直覺覺得的3。

爲何要異步更新?

由於this.setSatate()會觸發render生命週期鉤子,也就會運行組件的diff算法。若是每次setState都要走這一套流程,不只浪費性能,並且是徹底沒有必要的。

因此React選擇了在必定階段內批量更新。

仍是以生命週期爲界,掛載以前的全部setState批量更新,掛載以後到更新以前的全部setState批量更新,每次更新間隙的全部setState批量更新。

非異步狀況

再來看一種狀況:

猜猜最終渲染出來的star是多少?

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    timer = null;
    
    componentDidMount() {
        this.timer = setTimeout(() => {
            this.setState({ num: this.state.star + 1 });
            this.setState({ num: this.state.star + 1 });
            this.setState({ num: this.state.star + 1 });
        }, 5000);
    }
    
    componentWillUnmount() {
        clearTimeout(this.timer);
    }
}

export default App;

答案是3。

臥槽!

說實話,這裏我也沒想明白。

我在React倉庫的Issues裏提過這個狀況,這是React主創之一Dan Abramov的回答:

setState is currently synchronous outside of event handlers. That will likely change in the future.

Dan Abramov所說的event handlers應該指的是React合成事件回調和生命週期鉤子。

個人理解,由於只有這些方法才能迴應事件,因此它們之中的狀態更新是批量的。可是它們之中的異步代碼裏有狀態更新操做,React就不會批量更新,而是符合直覺的樣子。

咱們看下面的例子,正常的重複setState只會觸發一次更新,可是http請求回調中的重複setState卻會屢次觸發更新,看來異步的setState不在React掌控以內。

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        fetch('https://api.github.com/users/veedrin/repos')
            .then(res => res.json())
            .then(res => {
                console.log(res);
                this.setState({ star: this.state.star + 1 });
                this.setState({ star: this.state.star + 1 });
                this.setState({ star: this.state.star + 1 });
            });
    }
}

export default App;

還有一種狀況就是原生的事件回調,好比document上的事件回調,也不是異步的。

總結一下:所謂的異步只是批量更新而已。真正異步回調和原生事件回調中的setState不是批量更新的。

不過,Dan Abramov早就提到過,會在未來的某個版本(多是17大版本)管理全部的setState,不論是不是在所謂的event handlers以內。

React的設計有一種簡潔之美,從這種對待開發者反饋的態度可見一斑。

回調

既然this.setSatate()的設計不符合直覺,React早就爲開發者提供瞭解決方案。

this.setSatate()的參數既能夠是一個對象,也能夠是一個回調函數。函數返回的對象就是要更新的狀態。

回調函數提供了兩個參數,第一個參數就是計算過的state對象,即使這時尚未渲染,獲得的依然是符合直覺的計算過的值。同時,貼心的React還爲開發者提供了第二個參數,雖然並無什麼卵用。

如下,組件更新以後this.state.star的值是3。

有一個小細節:箭頭函數若是直接返回一個對象,要包裹一層小括號,以區別塊級做用域。

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        this.setState((prevState, prevProps) => ({ star: prevState.star + 1 }));
        this.setState((prevState, prevProps) => ({ star: prevState.star + 1 }));
        this.setState((prevState, prevProps) => ({ star: prevState.star + 1 }));
    }
}

export default App;

chaos

總之呢,React更新狀態的設計處處都是坑。

你們對React吐槽最多的點是什麼呢?

圈外人吐槽JSX。

圈內人吐槽this.setState

期盼React給開發者一個不使人困惑的狀態更新API吧。

React專題一覽

什麼是UI
JSX
可變狀態
不可變屬性
生命週期
組件
事件
操做DOM
抽象UI

相關文章
相關標籤/搜索