淺談 setState 更新機制

瞭解 React 同窗想必對setState函數是再熟悉不過了,setState也會常常做爲面試題,考察前端求職者對 React 的熟悉程度。html

在此我也拋一個問題,閱讀文章前讀者能夠先想一下這個問題的答案。前端

給 React 組件的狀態每次設置相同的值,如 setState({count: 1})。React 組件是否會發生渲染?若是是,爲何?若是不是,那又爲何?

1、場景復現

針對上述問題,先進行一個簡單的復現驗證。react

場景復現

如圖所示,App 組件有個設置按鈕,每次點擊設置按鈕,都會對當前組件的狀態設置相同的值{count: 1},當組件發生渲染時渲染次數會自動累加一,代碼以下所示:git

App 組件github

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 全局變量,用於記錄組件渲染次數
let renderTimes = 0;

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }

  handleClick = () => {
    this.setState({ count: 1 });
  };

  render() {
    renderTimes += 1;

    return (
      <div>
        <h3>場景復現:</h3>
        <p>每次點擊「設置」按鈕,當前組件的狀態都會被設置成相同的數值。</p>
        <p>當前組件的狀態: {this.state.count}</p>
        <p>
          當前組件發生渲染的次數:
          <span style={{ color: 'red' }}>{renderTimes}</span>
        </p>
        <div>
          <button onClick={this.handleClick}>設置</button>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

實際驗證結果以下所示,每次點擊設置按鈕,App 組件均會發生重複渲染。面試

場景復現操做

2、性能優化

那麼該如何減小 App 組件發生重複渲染呢?以前在 React 性能優化——淺談 PureComponent 組件與 memo 組件 一文中,詳細介紹了PureComponent的內部實現機制,此處可利用PureComponent組件來減小重複渲染。性能優化

實際驗證結果以下所示,優化後的 App 組件再也不產生重複渲染。less

性能優化

但這有個細節問題,可能你們平時工做中並未想過:dom

利用 PureComponent 組件可減小 App 組件的重複渲染,那麼是否表明 App 組件的狀態沒有發生變化呢?即引用地址是否依舊是上次地址呢?

廢話很少說,咱們針對這一問題進行下測試驗證,代碼以下:函數

APP 組件

import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';

// 全局變量,用於記錄組件渲染次數
let renderTimes = 0;
// 全局變量,記錄組件的上次狀態
let lastState = null;

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
    lastState = this.state; // 初始化,地址保持一致
  }

  handleClick = () => {
    console.log(`當前組件狀態是不是上一次狀態:${this.state === lastState}`);

    this.setState({ count: 1 });
    // 更新上一次狀態
    lastState = this.state;
  };

  render() {
    renderTimes += 1;

    return (
      <div>
        <h3>場景復現:</h3>
        <p>每次點擊「設置」按鈕,當前組件的狀態都會被設置成相同的數值。</p>
        <p>當前組件的狀態: {this.state.count}</p>
        <p>
          當前組件發生渲染的次數:
          <span style={{ color: 'red' }}>{renderTimes}</span>
        </p>
        <div>
          <button onClick={this.handleClick}>設置</button>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

在 APP 組件中,咱們經過全局變量lastState來記錄組件的上次狀態。當點擊設置按鈕時,會比較當前組件狀態與上一次狀態是否相等,即引用地址是否同樣?

引用地址變化

在 console 窗口中咱們發現,雖然 PureComponent組件減小了 App 組件的重複渲染,可是 App 組件狀態的引用地址卻發生了變化,這是爲何呢?

下面咱們將帶着這兩個疑問,結合 React V16.9.0 源碼,聊一聊setState的狀態更新機制。解讀過程當中爲了更好的理解源碼,會對源碼存在部分刪減。

3、setState 狀態更新機制

在解讀源碼的過程當中,整理了一份函數setState調用關係流程圖,以下所示:

setState更新機制

從上圖能夠看出,函數setState調用關係主要分爲如下兩個部分:

  • 將要更新的狀態添加到更新隊列中;
  • 產生一個調度任務。調度任務會遍歷更新隊列並計算出最終要更新的狀態,將其更新到組件實例中,而後完成組件渲染操做。

下面針對這兩個部分,結合源碼,進行下詳細闡述。

3.1 入更新隊列

3.1.1 setState 函數定義

摘自ReactBaseClasses.js文件。

Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

函數setState包含兩個參數partialStatecallback,其中partialState表示待更新的部分狀態,callback則爲狀態更新後的回調函數。

3.1.2 enqueueSetState 函數定義

摘自ReactFiberClassComponent.js文件。

enqueueSetState(inst, payload, callback) {
  const fiber = getInstance(inst);
  const currentTime = requestCurrentTime();
  const suspenseConfig = requestCurrentSuspenseConfig();
  const expirationTime = computeExpirationForFiber(
    currentTime,
    fiber,
    suspenseConfig,
  );

  // 建立一個update對象
  const update = createUpdate(expirationTime, suspenseConfig);
  // payload存放的是要更新的狀態,即partialState
  update.payload = payload;

  // 若是定義了callback,則將callback掛載在update對象上
  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }

  // ...省略...

  // 將update對象添加至更新隊列中
  enqueueUpdate(fiber, update);
  // 添加調度任務
  scheduleWork(fiber, expirationTime);
},

函數enqueueSetState會建立一個update對象,並將要更新的狀態partialState、狀態更新後的回調函數callback和渲染的過時時間expirationTime等都會掛載在該對象上。而後將該update對象添加到更新隊列中,而且產生一個調度任務。

若組件渲染以前屢次調用了setState,則會產生多個update對象,會被依次添加到更新隊列中,同時也會產生多個調度任務。

3.1.3 createUpdate 函數定義

摘自 ReactUpdateQueue.js文件。

export function createUpdate(
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
): Update<*> {
  let update: Update<*> = {
    expirationTime,
    suspenseConfig,

    // 添加TAG標識,表示當前操做是UpdateState,後續會用到。
    tag: UpdateState,
    payload: null,
    callback: null,

    next: null,
    nextEffect: null,
  };

  return update;
}

函數createUpdate會建立一個update對象,用於存放更新的狀態partialState、狀態更新後的回調函數callback和渲染的過時時間expirationTime

3.2 setState 狀態更新機制

從上圖能夠看出,每次調用setState函數都會建立一個調度任務。而後通過一系列函數調用,最終會調起函數updateClassComponent

圖中紅色區域涉及知識點較多,與咱們要討論的狀態更新機制關係不大,不是咱們這次的討論重點,因此咱們先行跳過,待後續研究(挖坑)。

下面咱們就簡單聊下組件實例的狀態是如何一步步完成更新操做的。

3.2.1 getStateFromUpdate 函數

摘自 ReactUpdateQueue.js文件。

function getStateFromUpdate<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  update: Update<State>,
  prevState: State,
  nextProps: any,
  instance: any,
): any {
  switch (update.tag) {

    // ....省略 ....

    // 見3.3節內容,調用setState會建立update對象,其屬性tag當時被標記爲UpdateState
    case UpdateState: {
      // payload 存放的是要更新的狀態state
      const payload = update.payload;
      let partialState;

      // 獲取要更新的狀態
      if (typeof payload === 'function') {
        partialState = payload.call(instance, prevState, nextProps);
      } else {
        partialState = payload;
      }

      // partialState 爲null 或者 undefined,則視爲未操做,返回上次狀態
      if (partialState === null || partialState === undefined) {
        return prevState;
      }

      // 注意:此處經過Object.assign生成一個全新的狀態state, state的引用地址發生了變化。
      return Object.assign({}, prevState, partialState);
    }

    // .... 省略 ....
  }

  return prevState;
}

getStateFromUpdate 函數主要功能是將存儲在更新對象update上的partialState與上一次的prevState進行對象合併,生成一個全新的狀態 state。

注意:

  • Object.assign 第一個參數是空對象,也就是說新的 state 對象的引用地址發生了變化。
  • Object.assign 進行的是淺拷貝,不是深拷貝。

3.2.2 processUpdateQueue 函數

摘自 ReactUpdateQueue.js文件。

export function processUpdateQueue<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  props: any,
  instance: any,
  renderExpirationTime: ExpirationTime,
): void {
  // ...省略...

  // 獲取上次狀態prevState
  let newBaseState = queue.baseState;

  /**
   * 若在render以前屢次調用了setState,則會產生多個update對象。這些update對象會以鏈表的形式存在queue中。
   * 如今對這個更新隊列進行依次遍歷,並計算出最終要更新的狀態state。
   */
  let update = queue.firstUpdate;
  let resultState = newBaseState;
  while (update !== null) {
    // ...省略...

    /**
     * resultState做爲參數prevState傳入getStateFromUpdate,而後getStateFromUpdate會合並生成
     * 新的狀態再次賦值給resultState。完成整個循環遍歷,resultState即爲最終要更新的state。
     */
    resultState = getStateFromUpdate(
      workInProgress,
      queue,
      update,
      resultState,
      props,
      instance,
    );
    // ...省略...

    // 遍歷下一個update對象
    update = update.next;
  }

  // ...省略...

  // 將處理後的resultState更新到workInProgess上
  workInProgress.memoizedState = resultState;
}

React 組件渲染以前,咱們一般會屢次調用setState,每次調用setState都會產生一個 update 對象。這些 update 對象會以鏈表的形式存在隊列 queue 中。processUpdateQueue函數會對這個隊列進行依次遍歷,每次遍歷會將上一次的prevState與 update 對象的partialState進行合併,當完成全部遍歷後,就能算出最終要更新的狀態 state,此時會將其存儲在 workInProgress 的memoizedState屬性上。

3.2.3 updateClassInstance 函數

摘自 ReactFiberClassComponent.js文件。

function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderExpirationTime: ExpirationTime,
): boolean {
  // 獲取當前實例
  const instance = workInProgress.stateNode;

  // ...省略...

  const oldState = workInProgress.memoizedState;
  let newState = (instance.state = oldState);
  let updateQueue = workInProgress.updateQueue;

  // 若是更新隊列不爲空,則處理更新隊列,並將最終要更新的state賦值給newState
  if (updateQueue !== null) {
    processUpdateQueue(
      workInProgress,
      updateQueue,
      newProps,
      instance,
      renderExpirationTime,
    );
    newState = workInProgress.memoizedState;
  }

  // ...省略...

  /**
   * shouldUpdate用於標識組件是否要進行渲染,其值取決於組件的shouldComponentUpdate生命週期執行結果,
   * 亦或者PureComponent的淺比較的返回結果。
   */
  const shouldUpdate = checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    );

  if (shouldUpdate) {
     // 若是須要更新,則執行相應的生命週期函數
     if (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
        typeof instance.componentWillUpdate === 'function') {
      startPhaseTimer(workInProgress, 'componentWillUpdate');
      if (typeof instance.componentWillUpdate === 'function') {
        instance.componentWillUpdate(newProps, newState, nextContext);
      }
      if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
        instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
      }
      stopPhaseTimer();
    }
    // ...省略...
  }

  // ...省略...

  /**
   * 無論shouldUpdate的值是true仍是false,都會更新當前組件實例的props和state的值,
   * 即組件實例的state和props的引用地址發生變化。也就是說即便咱們採用PureComponent來減小無用渲染,
   * 但並不表明該組件的state或者props的引用地址沒有發生變化!!!
   */
  instance.props = newProps;
  instance.state = newState;

  return shouldUpdate;
}

從上述代碼能夠看出,updateClassInstance函數主要實現瞭如下幾個功能:

  • 遍歷更新隊列,產生一個全新的 state,並將其更新至組件實例的 state 上;
  • 返回是否要進行更新的標識 shouldUpdate,該值的運行結果取決於shouldComponentUpdate生命週期函數執行結果或者PureComponent的淺比較結果;
  • 若是 shouldUpdate 的值爲true,則執行相應生命週期函數componentWillUpdate

此時要特別注意如下幾點:

  1. 組件實例的狀態 state 發生變化,即引用地址發生變化;
  2. 即便採用PureComponent或者shouldComponentUpdate來減小無用渲染,但組件實例的 props 或者 state 的引用地址也依舊發生了變化。

代碼解讀到此處,想必你們對以前提到的兩個疑問都有了答案吧。

3.2.4 updateClassComponent 函數

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps,
  renderExpirationTime: ExpirationTime,
) {
  // 獲取組件實例
  const instance = workInProgress.stateNode;

  // ...省略...

  let shouldUpdate;

  /**
   * 1. 完成組件實例的state、props的更新;
   * 2. componentWillUpdate、shouldComponentUpdate生命週期函數執行完畢;
   * 3. 獲取是否要進行更新的標識shouldUpdate;
   */
  shouldUpdate = updateClassInstance(
    current,
    workInProgress,
    Component,
    nextProps,
    renderExpirationTime,
  );

  /**
   * 1. 若是shouldUpdate值爲false,則退出渲染;
   * 2. 執行render函數
   */
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderExpirationTime,
  );

  // 返回下一個任務單元
  return nextUnitOfWork;
}

從上述代碼能夠看出,updateClassComponent函數主要實現瞭如下幾個功能:

  • 完成組件實例的 state、props 的更新;
  • 執行 componentWillUpdateshouldComponentUpdate等生命週期函數;
  • 完成組件實例的渲染;
  • 返回下一個待處理的任務單元;

4、小結

通過上章的代碼解讀,相信你們應該對函數setState應該有了全新的認識。以前提到的兩個疑問,應該都有了本身的答案。在此我簡單小結一下:

每次調用函數setState,react 都會將要更新的狀態添加到更新隊列中,併產生一個調度任務。調度任務在執行的過程當中會作兩個事情:

  • 遍歷更新隊列,計算出全新的狀態 state,更新到組件實例中;
  • 根據標識shouldUpdate來決定是否對組件實例進行從新渲染,而標識shouldUpdate的值則取決於PureComponent組件淺比較結果或者生命週期函數shouldComponentUpdate執行結果;

利用PureComponent組件能夠減小組件實例的重複渲染,但組件實例的狀態因爲被賦予了一個全新的狀態,因此引用地址發生了變化。

文章就暫時寫到這了,若是你們以爲博文還不錯,那就幫忙點個贊吧。

其餘:

相關文章
相關標籤/搜索