React setState 是同步更新還是異步更新?


轉載,網址:https://blog.csdn.net/qq_42033567/article/details/112005211

1. setState 基本使用

組件除了可以接收外界傳遞的狀態外,還可以擁有自己的狀態,並且這個狀態也可以通過 setState 來進行更新。setState 用於變更狀態,觸發組件重新渲染,更新視圖 UI。其語法如下:setState(updater, callback)
setState 可以接收兩個參數:第一個參數可以是對象或函數,第二個參數是函數。

第一個參數是對象的寫法:

this.setState({
  key: newState,
});

第一個參數是函數的寫法:

// prevState 是上一次的 state,props 是此次更新被應用時的 props
this.setState((prevState, props) => {
  return {
    key: prevState.key,
  };
});

那么,這兩種寫法的區別是什么呢?我們來看計數器的例子:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      val: 0,
    };
  }
  handleClick() {
    this.setState({
      val: this.state.val + 1,
    });
  }
  render() {
    return (
      <div className="App">
        <input type="text" value={this.state.val} disabled />
        <input type="button" onClick={this.handleClick.bind(this)} />
      </div>
    );
  }
}

如果在 handleClick 方法內調兩次 setState,是不是每次點擊就自增 2 了呢?

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

結果並非我們想的那樣,每次點擊按鈕,依然是自增 1。這是因為調用 setState 其實是異步的,也就是 setState 調用之后,this.state 不會立即映射為新的值。上面代碼會解析為以下形式:

// 后面的數據會覆蓋前面的更改,所以最終只加了一次.
Object.assign(previousState, { val: state.val + 1 }, { val: state.val + 1 });

在上面我們調用了兩次 setState,但 state 的更新會被合並,所以即使多次調用 setState,實際上可能也只是會重新渲染一次。

如果想基於當前的 state 來計算出新的值,那么 setState 第一個參數不應該像上面一樣傳遞一個對象,而應該傳遞一個函數。

handleClick () {
        this.setState(function (state, props) {
      return { val: state.val + 1 };
    });
    this.setState(function (state, props) {
      return { val: state.val + 1 };
    });
 }

此時,在 handleClick 方法內調兩次 setState,就能實現每次點擊都自增 2 了。

傳遞一個函數可以讓你在函數內訪問到當前的 state 值。 setState 的調用是分批的,所以可以鏈式地進行更新,並確保它們是一個建立在另一個之上的,這樣才不會發生沖突。

setState 的第二個參數是一個可選的回調函數。這個回調函數將在組件重新渲染后執行。等價於在 componentDidUpdate 生命周期內執行。通常建議使用 componentDidUpdate 來代替此方式。在這個回調函數中你可以拿到更新后 state 的值。

this.setState({
    key1: newState1,
    key2: newState2,
    ...
}, callback) // 第二個參數是 state 更新完成后的回調函數

通過上面內容,可以知道調用 setState 時,組件的 state 並不會立即改變, setState 只是把要修改的 state 放入一個隊列, React 會優化真正的執行時機,並出於性能原因,會將 React 事件處理程序中的多次 React 事件處理程序中的多次 setState 的狀態修改合並成一次狀態修改。 最終更新只產生一次組件及其子組件的重新渲染,這對於大型應用程序中的性能提升至關重要。
**
批量更新的流程圖如下:

this.setState({
  count: this.state.count + 1    ===>    入隊,[count+1的任務]
});
this.setState({
  count: this.state.count + 1    ===>    入隊,[count+1的任務,count+1的任務]
});
                                          ↓
                                         合並 state,[count+1的任務]
                                          ↓
                                         執行 count+1的任務

注意:在 React 中,不能直接使用 this.state.key = value 方式來更新狀態,這種方式 React 內部無法知道我們修改了組件,因此也就沒辦法更新到界面上。所以一定要使用 React 提供的 setState 方法來更新組件的狀態。

那 為什么 setState 是異步的,這個問題得到了 React 官方團隊的回復,原因有兩個:

    1. 保持內部一致性。如果改為同步更新的方式,盡管 setState 變成了同步,但是 props 不是。
      為后續的架構升級啟用並發更新。為了完成異步渲染,React 會在 setState 時,根據它們的數據來源分配不同的優先級,這些數據來源有:事件回調句柄、動畫效果等,再根據優先級並發處理,提升渲染性能。
    1. setState 同步場景
      上面的例子使我們建立了這樣一個認知:setState 是異步的,但下面這個案例又會顛覆你的認知。如果我們將 setState 放在 setTimeout 事件中,那情況就完全不同了:
class Test extends Component {
    state = {
        count: 0
    }
    componentDidMount(){
        this.setState({ count: this.state.count + 1 });
        console.log(this.state.count);
        setTimeout(() => {
          this.setState({ count: this.state.count + 1 });
          console.log("setTimeout: " + this.state.count);
        }, 0);
    }
    render(){
        ...
    }
}

這時就會輸出 0,2。因為 setState 並不是真正的異步函數,它實際上是通過隊列延遲執行操作實現的,通過 isBatchingUpdates 來判斷 setState 是先存進 state 隊列還是直接更新。值為 true 則執行異步操作,false 則直接同步更新。

在 onClick、onFocus 等事件中,由於合成事件封裝了一層,所以可以將 isBatchingUpdates 的狀態更新為 true;在 React 的生命周期函數中,同樣可以將 isBatchingUpdates 的狀態更新為 true。那么在 React 自己的生命周期事件和合成事件中,可以拿到 isBatchingUpdates 的控制權,將狀態放進隊列,控制執行節奏。而在外部的原生事件中,並沒有外層的封裝與攔截,無法更新 isBatchingUpdates 的狀態為 true。這就造成 isBatchingUpdates 的狀態只會為 false,且立即執行。所以在 addEventListener 、setTimeout、setInterval 這些原生事件中都會同步更新。

實際上,setState 並不是具備同步這種特性,只是在特定的情境下,它會從 React 的異步管控中“逃脫”掉。

3. 調用 setState 發生了什么

 修改 state 方法有兩種:
  • 1.構造函數里修改 state ,只需要直接操作 this.state 即可, 如果在構造函數里執行了異步操作,就需要調用 setState 來觸發重新渲染。
  • 2.在其余的地方需要改變 state 的時候只能使用 setState,這樣 React 才會觸發 UI 更新。
    所以, setState 時會設置新的 state 並更新 UI。當然,state 的更新可能是異步的,出於性能考慮,React 可能會把多個 setState 調用合並成一個調用。那么 state 的更新何時是同步何時又是異步的呢?

我們來看一下 setState 的執行流程圖:
img

(1)setState
下面來看下每一步的源碼,首先是 setState 入口函數:

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, "setState");
  }
};

入口函數在這里就是充當一個分發器的角色,根據入參的不同,將其分發到不同的功能函數中去。這里我們以對象形式的入參為例,可以看到它直接調用了 this.updater.enqueueSetState 這個方法。

(2)enqueueSetState

enqueueSetState: function (publicInstance, partialState) {
  // 根據 this 拿到對應的組件實例
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  // 這個 queue 對應的就是一個組件實例的 state 數組
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState);
  //  enqueueUpdate 用來處理當前的組件實例
  enqueueUpdate(internalInstance);
}

這里 enqueueSetState 做了兩件事:

  • 1.將新的 state 放進組件的狀態隊列里;
  • 2.用 enqueueUpdate 來處理將要更新的實例對象。

(3)enqueueUpdate

function enqueueUpdate(component) {
  ensureInjected();
  // 注意這一句是問題的關鍵,isBatchingUpdates標識着當前是否處於批量創建/更新組件的階段
  if (!batchingStrategy.isBatchingUpdates) {
    // 若當前沒有處於批量創建/更新組件的階段,則立即更新組件
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 否則,先把組件塞入 dirtyComponents 隊列里,讓它“再等等”
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

這個 enqueueUpdate 引出了一個關鍵的對象——batchingStrategy,該對象所具備的 isBatchingUpdates 屬性直接決定了當下是要走更新流程,還是應該排隊等待;其中的 batchedUpdates 方法更是能夠直接發起更新流程。由此可以推測,batchingStrategy 或許正是 React 內部專門用於管控批量更新的對象。

(4)batchingStrategy

var ReactDefaultBatchingStrategy = {
  // 全局唯一的鎖標識
  isBatchingUpdates: false,
  // 發起更新動作的方法
  batchedUpdates: function (callback, a, b, c, d, e) {
    // 緩存鎖變量
    var alreadyBatchingStrategy =
      ReactDefaultBatchingStrategy.isBatchingUpdates;
    // 把鎖“鎖上”
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingStrategy) {
      callback(a, b, c, d, e);
    } else {
      // 啟動事務,將 callback 放進事務里執行
      transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

batchingStrategy 對象可以理解為它是一個“鎖管理器”。

這里的“鎖”,是指 React 全局唯一的 isBatchingUpdates 變量,isBatchingUpdates 的初始值是 false,意味着“當前並未進行任何批量更新操作”。每當 React 調用 batchedUpdate 去執行更新動作時,會先把這個鎖給“鎖上”(置為 true),表明“現在正處於批量更新過程中”。當鎖被“鎖上”的時候,任何需要更新的組件都只能暫時進入 dirtyComponents 里排隊等候下一次的批量更新,而不能隨意“插隊”。此處體現的“任務鎖”的思想,是 React 面對大量狀態仍然能夠實現有序分批處理的基石。**

4. 總結

對於那道常考的面試題:setState 是同步更新還是異步更新? 我們心中或許已經有了答案。

setState 並不是單純同步/異步的,它的表現會因調用場景的不同而不同:在 React 鈎子函數及合成事件中,它表現為異步;而在 setTimeout、setInterval 等函數中,包括在 DOM 原生事件中,它都表現為同步。這種差異,本質上是由 React 事務機制和批量更新機制的工作方式來決定的。

在源碼中,通過 isBatchingUpdates 來判斷 setState 是先存進 state 隊列還是直接更新,如果值為 true 則執行異步操作,為 false 則直接更新。

那什么情況下 isBatchingUpdates 會為 true 呢?

在 React 可以控制的地方,isBatchingUpdates 就為 true,比如在 React 生命周期事件和合成事件中,都會走合並操作,延遲更新的策略。
在 React 無法控制的地方,比如原生事件,具體就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。
一般認為,做異步設計是為了性能優化、減少渲染次數,React 團隊還補充了兩點:

保持內部一致性。如果將 state 改為同步更新,那盡管 state 的更新是同步的,但是 props 不是。
啟用並發更新,完成異步渲染。
附一個常考的面試題:

class Test extends React.Component {
  state = {
    count: 0,
  };
  componentDidMount() {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count);
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count);
    }, 0);
  }
  render() {
    return null;
  }
}

首先第一次和第二次的 console.log,都在 React 的生命周期事件中,所以是異步的處理方式,則輸出都為 0;
而在 setTimeout 中的 console.log 處於原生事件中,所以會同步的處理再輸出結果,但需要注意,雖然 count 在前面經過了兩次的 this.state.count + 1,但是每次獲取的 this.state.count 都是初始化時的值,也就是 0;
所以此時 count 是 1,那么后續在 setTimeout 中的輸出則是 2 和 3。
所以答案是 0,0,2,3。


免責聲明!

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



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