你真的理解setState嗎?

面試官:「react中setState是同步的仍是異步?」
我:「異步的,setState不能立馬拿到結果。」javascript

面試官:「那什麼場景下是異步的,可不多是同步,什麼場景下又是同步的?」
我:「......」html

setState真的是異步的嗎?

這兩天本身簡單的看了下 setState 的部分實現代碼,在這邊給到你們一個本身我的的看法,可能文字或圖片較多,沒耐心的同窗能夠直接跳過看總結(源碼版本是16.4.1)。前端

看以前,爲了方便理解和簡化流程,咱們默認react內部代碼執行到performWorkperformWorkOnRootperformSyncWorkperformAsyncWork這四個方法的時候,就是react去update更新而且做用到UI上。java

1、合成事件中的setState

首先得了解一下什麼是合成事件,react爲了解決跨平臺,兼容性問題,本身封裝了一套事件機制,代理了原生的事件,像在jsx中常見的onClickonChange這些都是合成事件。react

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新前的val --> 0
  }
  render() {
    return (
      <div onClick={this.increment}> {`Counter is: ${this.state.val}`} </div>
    )
  }
}
複製代碼

合成事件中的setState寫法比較常見,點擊事件裏去改變 this.state.val 的狀態值,在 increment 事件中打個斷點能夠看到調用棧,這裏我貼一張本身畫的流程圖:git

合成事件中setState的調用棧
dispatchInteractiveEventcallCallBack 爲止,都是對合成事件的處理和執行,從 setStaterequestWork 是調用 this.setState 的邏輯,這邊主要看下 requestWork 這個函數(從 dispatchEventrequestWork 的調用棧是屬於 interactiveUpdates$1try 代碼塊,下文會提到)。

function requestWork(root, expirationTime) {
  addRootToSchedule(root, expirationTime);

  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpiration(expirationTime);
  }
}
複製代碼

requestWork 中有三個if分支,三個分支中有兩個方法 performWorkOnRootperformSyncWork ,就是咱們默認的update函數,可是在合成事件中,走的是第二個if分支,第二個分支中有兩個標識 isBatchingUpdatesisUnbatchingUpdates 兩個初始值都爲 false ,可是在 interactiveUpdates$1 中會把 isBatchingUpdates 設爲 true ,下面就是 interactiveUpdates$1 的代碼:github

function interactiveUpdates$1(fn, a, b) {
  if (isBatchingInteractiveUpdates) {
    return fn(a, b);
  }
  // If there are any pending interactive updates, synchronously flush them.
  // This needs to happen before we read any handlers, because the effect of
  // the previous event may influence which handlers are called during
  // this event.
  if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPendingInteractiveExpirationTime, false, null);
    lowestPendingInteractiveExpirationTime = NoWork;
  }
  var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingInteractiveUpdates = true;
  isBatchingUpdates = true;  // 把requestWork中的isBatchingUpdates標識改成true
  try {
    return fn(a, b);
  } finally {
    isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}
複製代碼

在這個方法中把 isBatchingUpdates 設爲了 true ,致使在 requestWork 方法中, isBatchingUpdatestrue ,可是 isUnbatchingUpdatesfalse ,而被直接return了。面試

那return完的邏輯回到哪裏呢,最終正是回到了 interactiveUpdates 這個方法,仔細看一眼,這個方法裏面有個try finally語法,前端同窗這個實際上是用的比較少的,簡單的說就是會先執行 try 代碼塊中的語句,而後再執行 finally 中的代碼,而 fn(a, b) 是在try代碼塊中,剛纔說到在 requestWork 中被return掉的也就是這個fn(上文提到的 從dispatchEventrequestWork 的一整個調用棧)。app

因此當你在 increment 中調用 setState 以後去console.log的時候,是屬於 try 代碼塊中的執行,可是因爲是合成事件,try代碼塊執行完state並無更新,因此你輸入的結果是更新前的 state 值,這就致使了所謂的"異步",可是當你的try代碼塊執行完的時候(也就是你的increment合成事件),這個時候會去執行 finally 裏的代碼,在 finally 中執行了 performSyncWork 方法,這個時候纔會去更新你的 state 而且渲染到UI上。less

2、生命週期函數中的setState

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 輸出的仍是更新前的值 --> 0
 }
  render() {
    return (
      <div> {`Counter is: ${this.state.val}`} </div>
    )
  }
}
複製代碼

鉤子函數中setState的調用棧:

其實仍是和合成事件同樣,當 componentDidmount 執行的時候,react內部並無更新,執行完 componentDidmount 後纔去 commitUpdateQueue 更新。這就致使你在 componentDidmountsetState 完去console.log拿的結果仍是更新前的值。

3、原生事件中的setState

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新後的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }
 
  render() {
    return (
      <div> {`Counter is: ${this.state.val}`} </div>
    )
  }
}

複製代碼

原生事件是指非react合成事件,原生自帶的事件監聽 addEventListener ,或者也能夠用原生js、jq直接 document.querySelector().onclick 這種綁定事件的形式都屬於原生事件。

原生事件的調用棧就比較簡單了,由於沒有走合成事件的那一大堆,直接觸發click事件,到 requestWork ,在 requestWork裏因爲 expirationTime === Sync 的緣由,直接走了 performSyncWork 去更新,並不像合成事件或鉤子函數中被return,因此當你在原生事件中setState後,能同步拿到更新後的state值。

4、setTimeout中的setState

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 輸出更新後的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div> {`Counter is: ${this.state.val}`} </div>
    )
  }
}
複製代碼

setTimeout 中去 setState 並不算是一個單獨的場景,它是隨着你外層去決定的,由於你能夠在合成事件中 setTimeout ,能夠在鉤子函數中 setTimeout ,也能夠在原生事件setTimeout,可是不論是哪一個場景下,基於event loop的模型下, setTimeout 中裏去 setState 總能拿到最新的state值。

舉個栗子,好比以前的合成事件,因爲你是 setTimeout(_ => { this.setState()}, 0) 是在 try 代碼塊中,當你 try 代碼塊執行到 setTimeout 的時候,把它丟到列隊裏,並無去執行,而是先執行的 finally 代碼塊,等 finally 執行完了, isBatchingUpdates 又變爲了 false ,致使最後去執行隊列裏的 setState 時候, requestWork 走的是和原生事件同樣的 expirationTime === Sync if分支,因此表現就會和原生事件同樣,能夠同步拿到最新的state值。

5、setState中的批量更新

class App extends Component {

  state = { val: 0 }

  batchUpdates = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
 }

  render() {
    return (
      <div onClick={this.batchUpdates}> {`Counter is ${this.state.val}`} // 1 </div>
    )
  }
}
複製代碼

上面的結果最終是1,在 setState 的時候react內部會建立一個 updateQueue ,經過 firstUpdatelastUpdatelastUpdate.next 去維護一個更新的隊列,在最終的 performWork 中,相同的key會被覆蓋,只會對最後一次的 setState 進行更新,下面是部分實現代碼:

function createUpdateQueue(baseState) {
  var queue = {
    expirationTime: NoWork,
    baseState: baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
  };
  return queue;
}

function appendUpdateToQueue(queue, update, expirationTime) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > expirationTime) {
    // The incoming update has the earliest expiration of any update in the
    // queue. Update the queue's expiration time.
    queue.expirationTime = expirationTime;
  }
}
複製代碼

看個🌰

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}
複製代碼

結合上面分析的,鉤子函數中的 setState 沒法立馬拿到更新後的值,因此前兩次都是輸出0,當執行到 setTimeout 裏的時候,前面兩個state的值已經被更新,因爲 setState 批量更新的策略, this.state.val 只對最後一次的生效,爲1,而在 setTimmoutsetState 是能夠同步拿到更新結果,因此 setTimeout 中的兩次輸出2,3,最終結果就爲 0, 0, 2, 3

總結 :

  1. setState 只在合成事件和鉤子函數中是「異步」的,在原生事件和 setTimeout 中都是同步的。
  2. setState的「異步」並非說內部由異步代碼實現,其實自己執行的過程和代碼都是同步的,只是合成事件和鉤子函數的調用順序在更新以前,致使在合成事件和鉤子函數中無法立馬拿到更新後的值,形式了所謂的「異步」,固然能夠經過第二個參數 setState(partialState, callback) 中的callback拿到更新後的結果。
  3. setState 的批量更新優化也是創建在「異步」(合成事件、鉤子函數)之上的,在原生事件和setTimeout 中不會批量更新,在「異步」中若是對同一個值進行屢次 setStatesetState 的批量更新策略會對其進行覆蓋,取最後一次的執行,若是是同時 setState 多個不一樣的值,在更新時會對其進行合併批量更新。

以上就是我看了部分代碼後的粗淺理解,對源碼細節的那塊分析的較少,主要是想讓你們理解setState在不一樣的場景,不一樣的寫法下到底發生了什麼樣的一個過程和結果,但願對你們有幫助,因爲是我的的理解和看法,若是哪裏有說的不對的地方,歡迎你們一塊兒指出並討論。

另外,幫朋友打個廣告 :

有好友整理了一波內推崗位,已發佈到github,感興趣的能夠聯繫cXE3MjcwNDAxNDE=

相關文章
相關標籤/搜索