本文是『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.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;
總之呢,React更新狀態的設計處處都是坑。
你們對React吐槽最多的點是什麼呢?
圈外人吐槽JSX。
圈內人吐槽this.setState
。
期盼React給開發者一個不使人困惑的狀態更新API吧。
React專題一覽