React菜鳥入門之setState

還好還好哈哈22發表於2019-03-01

一、setState 這個磨人的小妖精!

作為一名入職前基本沒有接觸過React的小菜鳥,在接手的第一個練手專案中,很快就遇到了許多React初學者都會遇到的問題-setState。

let promiseArr = [];
this.setState({
    topStoryIds: res.data
})
for (let i = 0; i < 30; i++)
{
    promiseArr.push(axios.get(`https://hacker-news.firebaseio.com/v0/item/` this.state.newStoryIds[i] .json?print=pretty`));
}
複製程式碼

最初的設想是先通過this.setState設定state中的資料,然後再取用this.state。當然,結果不出所料的悲劇了。。苦思不得,不停的打斷點,找原因,一度懷疑自己腦子瓦特了。。

image.png

就在我即將準備砸鍵盤的時候,突然想起來之前看文件的時候,好像提到過setState有個啥“非同步更新”的東東,如一道閃電劃過我的腦海。

二、setState是個啥

setState,是React官方推出的更新state的用法。通過呼叫setState,React能夠知道state發生了變化,並呼叫render方法將變化展現到檢視。

this.setState({
  count: this.state.count + 1, 
});
複製程式碼

瞭解了基本概念及用法,我們來看一下setState的注意點:

  • setState通過引發一次元件的更新過程來引發重新繪製;
  • 多次setState函式呼叫產生的效果會合並;
  • setState不會立刻改變React元件中state的值。

setState通過引發一次元件的更新過程來引發重新繪製
setState呼叫引起的React的更新生命週期函式4個函式(比修改prop引發的生命週期少一個componentWillReceiveProps函式),這4個函式依次被呼叫。

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

那麼state何時被更新呢,我們用一個小栗子來探索一哈。

  handleClick = () => {
    this.setState({ count: this.state + 1, });
  }

  shouldComponentUpdate() {
    console.log(`shouldComponentUpdate`,this.state.count);
    return true;
  }

  componentWillUpdate() {
    console.log(`componentWillUpdate`,this.state.count);
  }

  render() {
    console.log(`render`,this.state.count);
  }

  componentDidUpdate() {
    console.log(`componentDidUpdate`,this.state.count);
  }
複製程式碼

對應的控制檯資訊如下:

shouldComponentUpdate 0
componentWillUpdate 0
render [object Object]1
componentDidUpdate [object Object]1
複製程式碼

由此可知,state一直到render函式執行的時候,才會被更新,在這之前,state一直保持為更新前的狀態。(或者,當shouldComponentUpdate函式返回false,這時候更新過程就被中斷了,render函式也不會被呼叫了,這時候React不會放棄掉對this.state的更新的,所以雖然不呼叫render,依然會更新this.state。)

多次setState函式呼叫產生的效果會合並
Talk is cheap, show me your code!

handleClick = () => {
    this.setState({ count: this.state.count + 1, });
    console.log(`第一次加一`, this.state.count);
    this.setState({ count: this.state.count + 1, });
    console.log(`第二次加一`, this.state.count);
}

render() {
  console.log(`render加一`, this.state.count);    
  return (
    <div className="App">
      <button onClick={this.handleClick}>count + 1</button>
    </div>
  );
}
複製程式碼

對應的渲染得到的檢視即一個button:

image.png

單擊button後,控制檯的輸出結果如下:

第一次加一 0
第二次加一 0
render加一 [object Object]1
複製程式碼

如上所示,我們在handleClick中進行了兩次setState操作,但對應的render卻只執行了一次,說明了React將兩次setState合併為了一次,進行merge後統一更新。
其實想想也很容易理解,若每次setState都觸發一次更新行為的話,那麼將造成多麼大的效能浪費。所以,從效能角度考慮,setState“多次setState函式呼叫產生的效果會合並”這一特性是合理而有必要的。
但是,相信善於觀察的你一定會有一個疑問:為什麼上例中render列印的this.state.count不是2,而是1呢?明明handleClick中進行了兩次加一操作啊!這也是我最開始看到這個栗子時候的一個疑問。

三、setState的非同步更新機制

setState不會立刻改變React元件中state的值
其實,問題的答案在前面已經給出了,即“state一直到render函式執行的時候,才會被更新”。
回到上面的栗子中去:

handleClick = () => {
    this.setState({ count: this.state.count + 1, });
    console.log(`第一次加一`, this.state.count);
    this.setState({ count: this.state.count + 1, });
    console.log(`第二次加一`, this.state.count);
}
複製程式碼

表面上看,handleClick對this.state.count執行了兩次加一操作,最終的this.state.count應該等於2,但是,因為React的非同步更新機制,導致this.state.count直到render函式執行前依然未得到更新,即上面的兩次this.state.count + 1操作是冗餘,與下面的程式碼等價:

handleClick = () => {
    const count = this.state.count;
    this.setState({ count: count + 1, });
    console.log(`第一次加一`, this.state.count);
    this.setState({ count: count + 1, });
    console.log(`第二次加一`, this.state.count);
}
複製程式碼

顯而易見,這樣的更新是不會得到預想的結果的。此時的this.state.count只是對state的一個“快照”,不管執行多少次加一操作,其最終結果都只相當於一次。

四、setState會同步更新嗎?

setState函式存在的一個重要意義就是它可以驅動檢視的更新。如果僅僅想要改變state,我們可以直接對this.state物件進行操作:

  handleClick = () => {
    this.state.count++;
    console.log(`this.state.count: `, this.state.count);
  }

  render() {
    console.log(`觸發更新`);
    ...   
  }
複製程式碼

控制檯輸出:

this.state.count:  1
複製程式碼

可以看到,state值確實進行了操作,但是render函式卻沒有得到執行。這樣的更新操作是沒有意義的。
既然如此,那麼setState會進行同步更新嗎?答案是肯定的。
在React中,如果是由React引發的事件處理(比如通過onClick引發的事件處理),呼叫setState不會同步更新this.state,除此之外的setState呼叫會同步執行this.state。所謂“除此之外”,指的是繞過React通過addEventListener直接新增的事件處理函式,還有通過setTimeout/setInterval產生的非同步呼叫。
在React的setState函式實現中,會根據一個變數isBatchingUpdates判斷是直接更新this.state還是放到佇列中回頭再說,而isBatchingUpdates預設是false,也就表示setState會同步更新this.state,但是,有一個函式batchedUpdates,這個函式會把isBatchingUpdates修改為true,而當React在呼叫事件處理函式之前就會呼叫這個batchedUpdates,造成的後果,就是由React控制的事件處理過程setState不會同步更新this.state。
但是,setState的同步更新會導致嚴重的效能問題,我們在實際開發過程中應儘量避免使用。

五、耍個流氓?

既然直接操作this.state會導致無法觸發re-render過程,而setState又具有非同步更新這一讓人又愛又恨的特性,我們何不二者合二為一?

  handleClick = () => {
    this.state.count++;
    this.state.count++;
    this.state.count++;
    console.log(`this.state.count: `, this.state.count);
    this.setState({});
  }

  render() {
    console.log(`觸發更新了呢!`);   
    console.log(`render`,this.state.count);
    ...
  }
複製程式碼

控制檯資訊:

this.state.count:  3
觸發更新了呢!
render 3
複製程式碼
image.png

結果居然通過了!!!這樣的操作,既避免了非同步更新導致的同時多次更新state時的無效問題,又解決了直接操作this.state時不會觸發更新的問題,豈不是兩全其美?
這個問題仁者見仁,不同人眼中也許會有不同的答案。但我相信,React的作者設計這個框架的初衷肯定不在於此。

總結自setState:這個API設計到底怎麼樣 setState何時同步更新狀態

相關文章