你真的了解setState()嗎?

  • 2019 年 10 月 3 日
  • 筆記

React 中 setState()詳細解讀

對於 setState() 相信夥伴們都用過,它是 React 官方推薦用來更新組件 state 的 API,但是對於 setState() 你真的了解嗎?且待我慢慢詳聊一番。

setState() 官方用法指南

語法1: setState(updater[, callback])

  • updater:函數類型,返回一個更新後的 state 中的狀態對象,它會和 state 進行淺合併。

  • callback: 可選,回調函數。

語法2: setState(stateChange[, callback])

  • setState: 對象類型,會將傳入的對象淺層合併到新的 state 中。

  • callback:可選,回調函數。

對於這兩種形式,不同的是第一個參數選擇問題,可以選擇一個函數返回一個新的state對象,亦可以直接選擇一個對象應用於狀態更新,那麼啥時候選擇函數類型的參數,什麼時候選擇對象類型的呢?這裡可以總結兩句話:

  • 當前狀態更新無需依賴之前的state狀態時,選擇對象類型參數

  • 當前更新狀態依賴之前的狀態時,選擇函數類型參數

example:

<!DOCTYPE html>  <html lang="en">  <head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <meta http-equiv="X-UA-Compatible" content="ie=edge">    <title>setState詳解</title>    <script src="https://unpkg.com/react@16/umd/react.development.js"></script>    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>    <script src="https://unpkg.com/[email protected]/babel.min.js"></script>  </head>  <body>    <div id="app"></div>    <script type="text/babel">      class A extends React.Component {        state = {          count: 0        }        update1 = () => {          this.setState({count: this.state.count+1})        }          update2 = () => {          this.setState(state => ({            count: state.count+1          }))        }          update3 = () => {          this.setState({            count: 8          })        }          render () {          return (            <div>              <h1>{this.state.count}</h1>              <button onClick={this.update1} style={{marginRight: 15}}>測試1</button><button style={{marginRight: 15}} onClick={this.update2}>測試2</button><button onClick={this.update3}>測試3</button>            </div>          )        }      }      ReactDOM.render(      <A/>,      document.getElementById('app')      )    </script>  </body>  </html>

這個例子中,我們通過點擊按鈕測試1或測試2來改變組件 A 的 count 狀態值,因為每次修改狀態都是在原先基礎上加 1, 所以在setState 中適合選擇函數類型參數,即 update2 寫法推薦。

點擊 測試3 按鈕會直接將count 值修改為 固定值 8,這無需依賴上一次count狀態值,所以在setState 中適合選擇對象類型參數,即 update3 寫法推薦。

setState() 更新狀態一定是非同步的嗎?

我們知道setState() 會觸發組件render() 函數,重新渲染組件將更新後的內容顯示在視圖上,那麼在 setState() 之後我們立馬就能獲取到最新的state值嗎?

這裡涉及到一個 setState() 是非同步更新還是同步更新的問題?

結論:

  • 在React相關的回調函數中setState() 是非同步更新

  • 不在React 相關的回調中setState() 是同步更新

React 相關的回調包括:組件的生命周期鉤子,React 組件事件監聽回調。

React不相關的回調包括常見的:setTimeout(), Promise()等。

我們還是可以拿之前的按鈕點擊實例來測試。

example:

<!DOCTYPE html>  <html lang="en">  <head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <meta http-equiv="X-UA-Compatible" content="ie=edge">    <title>setState詳解</title>    <script src="https://unpkg.com/react@16/umd/react.development.js"></script>    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>    <script src="https://unpkg.com/[email protected]/babel.min.js"></script>  </head>  <body>    <div id="app"></div>    <script type="text/babel">      class A extends React.Component {        state = {          count: 0        }        update1 = () => {          this.setState({count: this.state.count+1})          console.log(this.state.count)        }          update2 = () => {          setTimeout(() => {            this.setState(state => ({              count: state.count+1            }))            console.log(this.state.count)          })        }          update3 = () => {          Promise.resolve().then(value => {            this.setState({              count: 8            })            console.log(this.state.count)          })        }          componentWillMount () {          this.setState(state => ({            count: state.count+1          }))          console.log(this.state.count)        }          render () {          console.log('render()', this.state.count)          return (            <div>            <h1>{this.state.count}</h1>              <button onClick={this.update1} style={{marginRight: 15}}>測試1</button><button style={{marginRight: 15}} onClick={this.update2}>測試2</button><button onClick={this.update3}>測試3</button>            </div>          )        }      }      ReactDOM.render(      <A/>,      document.getElementById('app')    )    </script>  </body>  </html>

我們在 React 事件監聽回調 update1 和 組件生命周期 componentWillMount() 鉤子裡面分別在setState()之後列印最新的 state 值,發現列印出來的還是修改之前的state,但是頁面已經更新為最新狀態,看圖:

採用同樣的方法我們可以觀察在 update2 的setTimeout() 和 update3 的 Promise() 回調中,setState() 後列印的是最新的state值,而且這個列印會在setState() 觸發組件重新render() 之後。經過測試,恰好驗證了我們的結論是正確的,在React 相關的回調中setState()是非同步更新狀態,在不相關的回調中 setState() 是同步更新狀態。

setState() 非同步更新狀態時,如何獲取最新的狀態值?

這個問題其實是針對當setState() 非同步更新狀態之後,怎麼立馬獲取到最新的狀態值,也就是上面例子我們說的在update1() 和componentWillMount()中怎麼列印出最新的state值。

答案其實非常簡單,也就是我們說到的setState()傳參的第二個callback() 參數。setState() 的第二個回調會在更新狀態之後,組件重新render() 之後調用,也就是這裡面我們可以獲取到最新的狀態值。

程式碼:

  ...    update1 = () => {    this.setState({count: this.state.count+1}, () => {      console.log(this.state.count)    })  }    componentWillMount () {    this.setState(state => ({      count: state.count+1    }), () => {        console.log(this.state.count)    })  }

這樣,我們同樣可以在update1 和 componentWillMount() 中 列印出最新的state值。

遇到重複多次調用setState(),React如何處理?

這裡我們討論的前提當然是setState() 非同步更新狀態時候,因為同步更新,我們調用幾次 setState(),就會觸發幾次 render鉤子,當然也會實時分別列印出更新後的狀態值。

結論:

這裡分兩種情況討論:

  • 當setState() 傳對象類型參數,React會合併重複多次的調用setState(),觸發一次render。

  • 當setState() 傳函數類型參數,React會依次多次的調用setState(),觸發一次render。

可以看到,我們多次重複調用setState(),不管是傳參是何種類型。React都只會調用一次 render,重新渲染組件。

我們可以同樣以按鈕點擊實例來測試我們結論。

example:

<!DOCTYPE html>  <html lang="en">  <head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <meta http-equiv="X-UA-Compatible" content="ie=edge">    <title>setState詳解</title>    <script src="https://unpkg.com/react@16/umd/react.development.js"></script>    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>    <script src="https://unpkg.com/[email protected]/babel.min.js"></script>  </head>  <body>    <div id="app"></div>    <script type="text/babel">      class A extends React.Component {        state = {          count: 0        }        update1 = () => {          // this.setState({count: this.state.count+1}, () => {          //   console.log(this.state.count)          // })          // this.setState({count: this.state.count+1}, () => {          //   console.log(this.state.count)          // })          // this.setState({count: this.state.count+1}, () => {          //   console.log(this.state.count)          // })          this.setState((state) => ({            count: state.count+1          }), () => {            console.log(this.state.count)          })          this.setState((state) => ({            count: state.count+1          }), () => {            console.log(this.state.count)          })          this.setState((state) => ({            count: state.count+1          }), () => {            console.log(this.state.count)          })        }          update2 = () => {          setTimeout(() => {            this.setState(state => ({              count: state.count+1            }))            console.log(this.state.count)            this.setState(state => ({              count: state.count+1            }))            console.log(this.state.count)            this.setState(state => ({              count: state.count+1            }))            console.log(this.state.count)          })        }          update3 = () => {          Promise.resolve().then(value => {            this.setState({              count: 8            })            console.log(this.state.count)          })        }          componentWillMount () {          this.setState(state => ({            count: state.count+1          }))          console.log(this.state.count)        }          render () {          console.log('render()', this.state.count)          return (            <div>            <h1>{this.state.count}</h1>              <button onClick={this.update1} style={{marginRight: 15}}>測試1</button><button style={{marginRight: 15}} onClick={this.update2}>測試2</button><button onClick={this.update3}>測試3</button>            </div>          )        }      }      ReactDOM.render(      <A/>,      document.getElementById('app')    )    </script>  </body>  </html>

當點擊測試按鈕2,因為setState() 是同步更新狀態,可以發現組件進行了多次render調用,分別依次列印出更新後的狀態值,這個很簡單。

我們點擊測試按鈕1,分別對傳給setState()參數不同進行了測試,發現當傳參是對象類型時候,React會合併重複setState()調用,也就是只更新一次state狀態,傳函數類型參數時候,則分別進行了計算更新。

無論以哪種方式傳參重複調用 setState() ,React 都只會進行一次render 調用,這也是性能優化的一部分,防止多次重複渲染帶來的性能問題。

其實官網推薦我們使用setState()時候,第一個參數傳函數類型參數,因為函數參數中接收的 state 和 props 都保證為最新。