setState非同步、同步與進階

Light_發表於2018-11-18

如何使用setState

在 React 日常的使用中,一個很重要的點就是,不要直接去修改 state。例如:this.state.count = 1是無法觸發 React 去更新檢視的。因為React的機制規定,一個state的更新,首先需要呼叫 setState 方法。

this.setState({
    count: 1
})
複製程式碼

這樣便能觸發重新渲染。稍有經驗的開發者會知道,setState 方法其實是 “非同步” 的。即立馬執行之後,是無法直接獲取到最新的 state 的,需要經過 React 對 state 的所有改變進行合併處理之後,才會去計算新的虛擬dom,再根據最新的虛擬dom去重新渲染真實dom。

class App extends Component {
	state = {
        count: 0
	}

    componentDidMount(){
        this.setState({count: this.state.count + 1})
        console.log(this.state.count) // 0
    }

    render(){
        ...
    }
}
複製程式碼

demo請點選

那怎麼才能獲取到修改後的state呢?React為我們提供了一個回撥去實現。

...
this.setState({count: this.state.count + 1}, ()=>{
    console.log(this.state.count) // 1
})
...
複製程式碼

回撥裡的 state 便是最新的了,原因是該回撥的執行時機在於state合併處理之後。如果我們這樣去做:

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

實際最終的 count 會等於 1,原因是執行時得到的 this.state.count = 0。那怎麼實現結果為 2 呢?

...
this.setState(prevState => {count: prevState.count + 1});
this.setState(prevState => {count: prevState.count + 1});
...
複製程式碼

setState()實際上可以接受一個函式作為引數,函式的首個引數就是上一次的state。

以上介紹了setState的三種使用方式,下面我們來看看它們的執行時機是怎樣的:

...
	this.setState({ count: this.state.count + 1 });
    console.log("console: " + this.state.count); // 0
    this.setState({ count: this.state.count + 1 }, () => {
      console.log("console from callback: " + this.state.count); // 2
    });
    this.setState(prevState => {
      console.log("console from func: " + prevState.count); // 1
      return {
        count: prevState.count + 1
      };
    }, ()=>{
      console.log('last console: '+ this.state.count)
    });
...
複製程式碼

執行結果:

console: 0 
console from func: 1 
console from callback: 2
last console: 2 
複製程式碼

React 其實會維護著一個 state 的更新佇列,每次呼叫 setState 都會先把當前修改的 state 推進這個佇列,在最後,React 會對這個佇列進行合併處理,然後去執行回撥。根據最終的合併結果再去走下面的流程(更新虛擬dom,觸發渲染)。

setState為什麼要設計成非同步的

因為setState()之後無法立馬獲取最新的 state,給人的感覺便是非同步去設定狀態。也確實是有非同步的感覺(實 際原理後面講訴)。那麼為什麼 React 要把狀態的更新設計成這種方式呢?直接 this.state.count = 1不好嗎?

有興趣的可以點選看看:github.com/facebook/re…

這邊簡單總結下:

  • 保證內部的一致性:即使state是同步更新,props也不是。(你只有在父元件重新渲染時才能知道props
  • state的更新延緩到最後批量合併再去渲染對於應用的效能優化是有極大好處的,如果每次的狀態改變都去重新渲染真實dom,那麼它將帶來巨大的效能消耗。

setState真的是非同步嗎

我們先來看一段程式碼,執行前建議大家先預估下結果:

demo請點選

class App extends Component {
  state = {
    count: 0
  };

  componentDidMount() {
    // 生命週期中呼叫
    this.setState({ count: this.state.count + 1 });
    console.log("lifecycle: " + this.state.count);
    setTimeout(() => {
      // setTimeout中呼叫
      this.setState({ count: this.state.count + 1 });
      console.log("setTimeout: " + this.state.count);
    }, 0);
    document.getElementById("div2").addEventListener("click", this.increment2);
  }

  increment = () => {
    // 合成事件中呼叫
    this.setState({ count: this.state.count + 1 });
    console.log("react event: " + this.state.count);
  };

  increment2 = () => {
    // 原生事件中呼叫
    this.setState({ count: this.state.count + 1 });
    console.log("dom event: " + this.state.count);
  };

  render() {
    return (
      <div className="App">
        <h2>couont: {this.state.count}</h2>
        <div id="div1" onClick={this.increment}>
          click me and count+1
        </div>
        <div id="div2">click me and count+1</div>
      </div>
    );
  }
}
複製程式碼

探討前,我們先簡單瞭解下react的事件機制:react為了解決跨平臺,相容性問題,自己封裝了一套事件機制,代理了原生的事件,像在jsx中常見的onClickonChange這些都是合成事件。

那麼以上4種方式呼叫setState(),後面緊接著去取最新的state,按之前講的非同步原理,應該是取不到的。然而,setTimeout中呼叫以及原生事件中呼叫的話,是可以立馬獲取到最新的state的。根本原因在於,setState並不是真正意義上的非同步操作,它只是模擬了非同步的行為。React中會去維護一個標識(isBatchingUpdates),判斷是直接更新還是先暫存state進佇列。setTimeout以及原生事件都會直接去更新state,因此可以立即得到最新state。而合成事件和React生命週期函式中,是受React控制的,其會將isBatchingUpdates設定為 true,從而走的是類似非同步的那一套。

總結

此處總結是直接引用了:juejin.im/post/5b45c5…

  1. setState 只在合成事件和鉤子函式中是“非同步”的,在原生事件和 setTimeout 中都是同步的。
  2. setState的“非同步”並不是說內部由非同步程式碼實現,其實本身執行的過程和程式碼都是同步的,只是合成事件和鉤子函式的呼叫順序在更新之前,導致在合成事件和鉤子函式中沒法立馬拿到更新後的值,形式了所謂的“非同步”,當然可以通過第二個引數 setState(partialState, callback) 中的callback拿到更新後的結果。
  3. setState 的批量更新優化也是建立在“非同步”(合成事件、鉤子函式)之上的,在原生事件和setTimeout 中不會批量更新,在“非同步”中如果對同一個值進行多次 setStatesetState 的批量更新策略會對其進行覆蓋,取最後一次的執行,如果是同時 setState 多個不同的值,在更新時會對其進行合併批量更新。

參考:

坦白說,自己的水平還是挺有限的,還需要努力努力再努力。只有懂了很多,才能寫出有意義的東西吧。現在寫的,大多都是根據別人的文章,自己理解了之後,換成自己的語言再表達一遍而已。。

慢慢來,總有一天可以突破這個界限的,真正寫出屬於自己的東西。

另外,真心感謝這些文章的作者給予的幫助。

關於我

微訊號:rcgrcg,歡迎交友~

微訊號 rcgrcg

為了生計,我也接外包專案的哦~

網站搭建(PC、H5、前後端全包,我們有團隊的哦),APP開發(安卓和IOS),都是有成功案例的哦。

有興趣的請聯絡我!!服務包您滿意的那種!!

Good luck!
2018-11-18 廈門

相關文章