react中的setState是同步還是異步?react為什么要將其設計成異步?


壹 ❀ 引

了解react的同學都知道,react遵守渲染公式UI=Render(state),狀態決定了組件UI最終渲染的樣子(props也可以理解為外部傳入的狀態),由此可見state對於react的重要性。而在實際使用中,若我們想修改狀態必須得借用APIsetState,也只有通過此方法修改狀態才能順利觸發react下次render,那么對於一個使用如此高頻的方法你了解它多少呢?

這里我們可以先拋出幾個問題:

  • setState是同步還是異步?
  • 什么情況下同步?什么情況下異步?
  • setState批量合並是指只執行最后一次嗎?比如執行了3次,第1,2次到底有沒有執行?
  • 為什么要將setState設計成異步?這樣設計的好處是什么?

設想一下,上述問題如果在面試中遇到該如何作答?那就讓我們帶着問題出發,另外,文中所有例子強烈建議本地跑一跑,加深對於概念的理解,那么本文開始。

貳 ❀ setState中的同步與異步

貳 ❀ 壹 updater為對象時的異步情況

setState接受一個帶有形式參數的 updater 函數(也可能直接是一個對象)與一個回調callback(可選)。

setState(updater, [callback])

官方明確表示,setState對於this.state並不是立刻更新,若在調用setState后想要立刻獲取到最新的this.state,那么建議在setStatecallback或者聲明周期componentDidUpdate中獲取,比如:

class Echo extends React.Component {
  state = {
    num: 1
  }

  componentDidUpdate() {
    console.log(this.state.num);//2
  }

  handleOnClick = () => {
    this.setState({ num: this.state.num + 1 }, () => {
      console.log(this.state.num);//2
    });
    console.log(this.state.num);//1
  }

  render() {
    return (
      <>
        <div>{this.state.num}</div>
        <button onClick={this.handleOnClick}>加1</button>
      </>
    )
  }
}

其實既然官方特意強調在callback中獲取最新的this.state,那就已經說明存在某些地方拿不到最新的this.state的情況,比如上述代碼中setState后我們立刻讀取sum,可以發現num還是1,那么到這里我們可以得知setState對於this.state的更新確實是異步。

問題來了,react為什么將setState設計成異步呢?設想下我們有如下這種場景:

class Echo extends React.Component {
  state = {
    num: 1
  }

  componentDidUpdate() {
    console.log(this.state.num);//2
  }

  handleOnClick = () => {
    this.setState({
      num: this.state.num + 1
    }, () => {
      console.log(this.state.num)//2
    });

    this.setState({
      num: this.state.num + 1
    }, () => {
      console.log(this.state.num)//2
    });

    console.log(this.state.num);//1
  }

  render() {
    return (
      <>
        <div>{this.state.num}</div>
        <button onClick={this.handleOnClick}>加1</button>
      </>
    )
  }
}

當點擊按鈕,我們需要連着兩次執行setState,那么react會幫我們修改兩次this.state然后重新render兩次嗎?很明顯並不是,react會批量合並多次setState操作,上述例子num最終是2,且render在點擊后只會渲染一次。

React在開始重新渲染之前, 會有意地進行"等待",直到所有在組件的事件處理函數內調用的 setState()都完成之后再做最終的this.state變更,這樣可以通過避免不必要的重新渲染來提升性能。

貳 ❀ 貳 updater為函數時的異步情況

突然奇想,上述代碼的需求有了些許變更,我們還是在點擊后執行兩次setState,但我預期最終的sum是3,如何做到呢?別忘了前面我們對於setState的語法介紹,本質上updater是一個接受最新state與最新props並用於返回你用來更新this.state的函數:

// 這里可以拿到最新的state與props,注意,是最新的state,而不是最新的this.state
(state, props) => stateChange

函數寫法能讓我們拿到立刻變更后的state,因此我們可以來看看這個例子:

class Echo extends React.Component {
  state = {
    num: 1
  }

  componentDidUpdate() {
    console.log('我是更新完成后的this.state',this.state.num);
  }

  handleOnClick = () => {
    this.setState((state, props) => {
      console.log('第一次調用,我是最新的state',state.num)
      console.log('第一次調用,我是當前的this.state',this.state.num)
      // 注意,這里用的是state,不是this.state
      return { num: state.num + 1 };
    }, () => {
      console.log('第一次調用,我是調用完成后的this.state',this.state.num)
    });

    this.setState((state, preProps) => {
      console.log('第二次調用,我是最新的state',state.num)
      console.log('第二次調用,我是當前的this.state',this.state.num)
      return { num: state.num + 1 };
    }, () => {
      console.log('第二次調用,我是調用完成后的this.state',this.state.num)
    });

    console.log('我用於檢驗異步,此時拿不到最新的this.state',this.state.num);//1
  }

  render() {
    console.log('用於檢驗render了幾次');
    return (
      <>
        <div>{this.state.num}</div>
        <button onClick={this.handleOnClick}>加1</button>
      </>
    )
  }
}

請問每次setState時的statethis.state是多少,更新完成后最終的this.state是多少?render會執行幾次呢?先思考下,答案如下:

最終this.state是3,且每次setState中拿到的state(注意不是this.state)都是我們預期修改后的,而且根據調用順序來看,雖然確實執行了多次setState,但最終對於this.state的修改只有一次,且render只執行了一次,這種情況下react依舊做了批量合並處理。

貳 ❀ 叄 批量合並是只執行最后一次setState嗎?

在上述例子中,我們在setState執行了多次this.state.num+1的操作,但最后this.state.num是2,那么請問,所謂批量合並,是只執行了其中某一次的setState嗎?執行的是第一次還是最后一次?其實看個例子就懂了:

class Echo extends React.Component {
  state = {
    a:false,
    b:false
  }

  componentDidUpdate() {
    console.log(this.state);
  }

  handleOnClick = () => {
    this.setState({
      a:true
    }, () => {
      console.log(this.state)
    });

    this.setState({
      b: true
    }, () => {
      console.log(this.state)
    });

    this.setState({
      a: false
    }, () => {
      console.log(this.state)
    });
  }

  render() {
    return (
      <>
        <button onClick={this.handleOnClick}>click me</button>
      </>
    )
  }
}

事實證明,setState在批量合並過程中還是會是執行每個setState,但在updater是對象的情況下,setState對於相同key的操作始終以最后一次修改為准:

// 執行了三次了加1,但最終其實只會加一次
this.setState({num:this.state.num + 1});
this.setState({num:this.state.num + 1});
this.setState({num:this.state.num + 1});

比如上述代碼執行了三次+1操作,等待渲染結束后,我們會發現結果num其實只加了一個1,它等同於:

const num = this.state.num;
this.setState({num:num + 1});
this.setState({num:num + 1});
this.setState({num:num + 1});

這里的const num = this.state.num; 就相當於是一個快照,setState確實執行了三次,只是設置的一直都是相同的值,導致最終this.state的值確確實實是以最后一次為准。

貳 ❀ 叄 什么情況下setState是同步?

其實要回到這個問題,我們只需要知道什么情況下setState是異步,那么反過來的情況自然就都是同步了。一般來說,react在事件處理函數內部的 setState 都是異步的,比如合成事件onClickonBlur,其次react提供的生命周期鈎子函數中也是異步。

那么是不是說只要setState不在合成事件內調用,我們就能實現同步更新了呢?來看個例子:

class Echo extends React.Component {
  state = {
    num:1
  }

  componentDidUpdate() {
    console.log(this.state.num);//2 3 4
  }

  handleOnClick = () => {
    setTimeout(()=>{
      this.setState({num:this.state.num+1});
      this.setState({num:this.state.num+1});
      this.setState({num:this.state.num+1});
      console.log(this.state.num);//4
    })
  }

  render() {
    console.log('我在render了');// 執行3次
    return (
      <>
        <button onClick={this.handleOnClick}>click me</button>
      </>
    )
  }
}

事實上,超出了react能力范疇之外的上下文,比如原生事件,定時器回調等等,在這里面進行setState操作都會同步更新state。比如在上述例子中,我們實現了在setState后獲取到同步更新的this.state,但遺憾的是,react此時並不能做到批量合並操作,導致render執行了三次。

貳 ❀ 肆 為什么一定要設計成異步,同步批量處理不行嗎?

其實在眾多react使用者中一直有一個這樣的疑問,雖然我們知道異步本質目的是為了異步累積處理setState,達到減少render提升性能的目的。那么問題來了,異步能做到批量處理,同步難道就不行嗎?我們讓state同步更新,只要在最終render時做好把控,不是一樣能達到這樣的效果,而且從代碼可讀上來說,同步更利於狀態維護。對此,官方給出了合理解釋,大致分為三點:

  • 保證內部的一致性

    即便setState能做到同步,react對於props的更新依舊是異步,這是因為對於一個子組件而言,它只有等到父組件重新渲染了,它才知道最新的props是多少,所以讓setState異步的另一個原因是為了讓state,props,refs更新的行為與表現保持一致。我們假設有下面這段代碼,它是同步執行:

    console.log(this.state.value);//0
    this.setState({value:this.state.value+1});
    console.log(this.state.value);//1
    this.setState({value:this.state.value+1});
    console.log(this.state.value);//2
    

    但現在我們有個場景,這個狀態需要被多個兄弟組件使用,因此我們需要將其狀態提升到父組件,以便於給多個兄弟組件共享:

    console.log(this.props.value);//0
    this.props.onIncrement();
    console.log(this.props.value);//0
    this.props.onIncrement();
    console.log(this.props.value);//0
    

    很遺憾上述代碼並不能按照我們預期的執行,因為在同步模型中,this.state會立刻更新,但是this.props並不會,而且在沒有重新渲染父組件的情況下,我們沒辦法立刻更新this.props,那要假設要做到每執行一次onIncrement能讓兄弟組件都拿到最新的props,唯一的辦法就是立刻重新渲染父組件,而這種場景下,已經與我們最初的批量合並處理減少重復渲染相違背了。

    而為了解決這個問題,reactthis.propsthis.state更新設計為異步,這也讓狀態提升時對於狀態的管理更合理與更安全。

  • 性能優化

    如果setState是同步的話,那么對於狀態的改變一定會按照setState調用順序來執行並改變,但事實上react會根據setState不同的調用源,為這些setState分配不同的優先級,調用源包含事件處理,網絡請求,動畫等等。

    官方給了一個這樣的例子,比如我們在一個聊天窗口聊天,輸入的信息變化會觸發setState,而此時我們搜到了一條新消息,新消息也會觸發setState,那么這里更好的做法是延遲新消息的setState的執行,降低其優先級,這樣就能避免輸入過程中因為新消息觸發的渲染,導致輸入過程中抖動以及延遲。如果給某些更新分配更低的優先級,那么就可以把它們拆分成幾毫秒的渲染塊,這樣用戶也不會察覺到。

  • 異步創造更多可能性

    異步除了性能優化之外,異步也為未來的react升級埋下更多可能性。比如我們有個需要,需要從頁面A導航到頁面B,那么這時候你可能需要做一個加載動畫,等待B頁面渲染。但如果導航切換特別快,閃爍一下加載動畫又會降低用戶體驗。

    而站在異步的基礎上,當我們調用setState去渲染一個新頁面,因為異步的緣故,react可以在后台渲染這個新頁面,而且不去阻塞舊頁面的交互,假設等待時間過長,我們還是可以展示loading,但如果等待耗時非常短暫,setState可以因為異步批量合並的緣故減少渲染,不會讓頁面頻繁閃動,從而提升用戶體驗。

對於問題的原回答,可閱讀此issues:RFClarification: why is setState asynchronous?那么到這里,我們站在react官方的角度解釋了為什么react中的setState是異步而不能是同步。

叄 ❀ 總

我們花了較大的篇幅解釋了好幾個setState相關非常意思的問題,但其實我們還剩余一個問題沒解釋,那就是像合成事件中的setState會異步執行批量合並操作,而像原生定時器中的setState卻不會如此。那么react如何區分這兩者情況,或者說react在合成事件的底層到底做了什么?

考慮到篇幅的問題,這個問題我打算放在與setState異步緊急相連的合成事件篇章去解釋,也便於大家對於本篇知識點的快捷梳理與消化。請回到文章開頭再次面對最初的那幾個答案,那么現在你心中是否有了自己的答案?

文章最后附上非常經典的setState點三次的問題,代碼如下:

class Echo extends React.Component{
  state = {
    count: 0
  }

	// count +1
  increment = () => {
    console.log('increment setState前的count', this.state.count)
    this.setState({
      count: this.state.count + 1
    });
    console.log('increment setState后的count', this.state.count)
  }
	
  // count +1 三次
  triple = () => {
    console.log('triple setState前的count', this.state.count)
    this.setState({
      count: this.state.count + 1
    });

    this.setState({
      count: this.state.count + 1
    });

    this.setState({
      count: this.state.count + 1
    });
    console.log('triple setState后的count', this.state.count)
  }
	// count - 1
  reduce = () => {
    setTimeout(() => {
      console.log('reduce setState前的count', this.state.count)
      this.setState({
        count: this.state.count - 1
      });
      console.log('reduce setState后的count', this.state.count)
    }, 0);
  }

  render(){
    return <div>
      <button onClick={this.increment}> +1 </button>
      <button onClick={this.triple}> +1 三次 </button>
      <button onClick={this.reduce}> -1 </button>
    </div>
  }
}

大家可以自行思考,如果此時你已經能輕松回答,那么你對於setState同步異步問題已經有了一個清晰的認知了。剩余的問題,我們在下一篇合成事件再詳細闡述,那么到這里本文結束。

參考

React setState 異步真的只是為了性能嗎?

React setState 同步異步背后的故事

react官網setState實際做了什么?

setState是同步的還是異步的?

React 中setState更新state何時同步何時異步?

React 中 setState() 為什么是異步的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM