1. 程式人生 > >React之setState呼叫unmount元件報警告

React之setState呼叫unmount元件報警告

最近公司比較忙,所以更新進度比較慢,一是因為要梳理以前的程式碼,二是因為趕進度!!!
這個問題也是機緣巧合碰到了,正趕上這周公司網路無緣無故抽風(很慢),然後那天在除錯程式碼的時候忽然發現瀏覽器報了一個這樣的Warning:

warn.png

 

其實從字面上也大概可以瞭解個大概,是由於setState只能更新掛載完成的元件或者正在掛載的元件,那麼定位這個BUG的第一步當然是先了解元件的生命週期

生命週期

之前我的一片文章其實已經淺略的介紹了下生命週期,這裡在補充下,facebook官方把生命週期分為三類,每類對應的函式分別如下:

Mounting:
constructor()
componentWillMount()
render()
componentDidMount()

Updating:
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()

Unmounting:
componentWillUnmount()

其中這三大類對應的函式中帶有Will的會在render之前呼叫,帶有Did的會在render之後呼叫,這裡著重強調一下shouldComponentUpdate()這個生命週期,因為這個生命週期的返回值將會影響其他生命週期是否執行,其中最值得關注的就是當返回flase的時候不會觸發render(另外還有componentWillUpdate() componentDidUpdate()),所以這也就給了我們優化專案的空間.由於題目的報錯是指我在Unmounting的時候呼叫了setState,所以我去排查專案,以為自己手滑寫錯了......但是結果我排查的時候發現我程式碼中並沒有在componentWillUnmount()這個生命週期鉤子裡面做setState的邏輯,於是好奇心之下我就在每個生命週期鉤子中都用了下setState,於是發現了一些有意思的事

有意思的事

code.png

 

console.png


這是在componetWillUpdate鉤子中寫了setState之後的效果,當receive的時候就會無限觸發componetWillUpdate,這是為什麼呢? 經過程式媛的幫助 @眼角淺藍(連結在後面)http://www.jianshu.com/u/8385c9b70d89,加上自己翻看原始碼,大概理解如下:

 

React原始碼中有這樣一個函式:

performUpdateIfNecessary.png

 

這個函式負責update元件,我們可以看到當this._pendingStateQueue !=null 時會觸發this.updateComponent, this.updateComponent函式的程式碼如下:

updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext,
  ) {
    var inst = this._instance;
    invariant(
      inst != null,
      'Attempted to update component `%s` that has already been unmounted ' +
        '(or failed to mount).',
      this.getName() || 'ReactCompositeComponent',
    );

    var willReceive = false;
    var nextContext;

    // Determine if the context has changed or not
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    var prevProps = prevParentElement.props;
    var nextProps = nextParentElement.props;

    // Not a simple state update but a props update
    if (prevParentElement !== nextParentElement) {
      willReceive = true;
    }

    // An update here will schedule an update but immediately set
    // _pendingStateQueue which will ensure that any state updates gets
    // immediately reconciled instead of waiting for the next batch.
    if (willReceive && inst.componentWillReceiveProps) {
      if (__DEV__) {
        measureLifeCyclePerf(
          () => inst.componentWillReceiveProps(nextProps, nextContext),
          this._debugID,
          'componentWillReceiveProps',
        );
      } else {
        inst.componentWillReceiveProps(nextProps, nextContext);
      }
    }

    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;

    if (!this._pendingForceUpdate) {
      if (inst.shouldComponentUpdate) {
        if (__DEV__) {
          shouldUpdate = measureLifeCyclePerf(
            () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
            this._debugID,
            'shouldComponentUpdate',
          );
        } else {
          shouldUpdate = inst.shouldComponentUpdate(
            nextProps,
            nextState,
            nextContext,
          );
        }
      } else {
        if (this._compositeType === CompositeTypes.PureClass) {
          shouldUpdate =
            !shallowEqual(prevProps, nextProps) ||
            !shallowEqual(inst.state, nextState);
        }
      }
    }

    if (__DEV__) {
      warning(
        shouldUpdate !== undefined,
        '%s.shouldComponentUpdate(): Returned undefined instead of a ' +
          'boolean value. Make sure to return true or false.',
        this.getName() || 'ReactCompositeComponent',
      );
    }

    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext,
      );
    } else {
      // If it's determined that a component should not update, we still want
      // to set props and state but we shortcut the rest of the update.
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  },

程式的最後幾行我們可以知道當需要shouldUpdate的時候會執行this._performComponentUpdate這個函式,this._performComponentUpdate函式的原始碼如下:

 

_performComponentUpdate.png

 

我們可以發現這個方法在次呼叫了componentWillUpdate方法,但是當在componentWillUpdate週期中呼叫setState時就會觸發最上邊的performUpdateIfNecessary函式,所以一直迴圈下去...
附一個生命週期執行順序的圖:

lifeCycle.png

 

這裡通過原始碼在附一張生命週期是否可以寫setState函式的圖(這裡的可以不可以不是指報錯,而是指是否有必要或者是避免出現無限迴圈):

image.png

 

es6語法中getDefaultProps和getInitialState合併到構造器constrator中.

回到主題

感覺扯著扯著有點遠了,那麼這個bug怎麼解決呢(我們暫且成為bug,其實不會影響程式執行),到底是怎麼產生的呢,原來是因為我有一部分setState寫在了fetch的回撥函式裡,但fetch還沒結束時我已經解除安裝了這個元件,當請求結束後setState執行的時候會發現這個元件已經解除安裝了,所以才會報了這個warning...

解決辦法

1.把所有類似這種的請求放在專案中那個永遠不會解除安裝的元件裡,比如最頂層元件,然後資料通過props分發傳遞,這也就是同樣的邏輯為什麼通過redux的dispatch不會報warning,因為redux其實就一個最頂層的state,然後元件通過connect把值與props相關聯起來
2.第一種方法很麻煩,我現在採用的也是第二種,這裡要提一下,其實react早就知道這個問題了,所以在最開始版本有這樣一個引數叫isMounted,大家可以列印一下元件裡的this,就應該能看的到,與這個一起的還有一個引數叫做replaceState,isMounted這個函式返回true和false,代表元件是否已經解除安裝,有了這個引數之後我們就可以在fecth的回撥函式裡在新增一個判斷,只有元件未解除安裝才觸發 setState,但是很不巧,這兩個api都被廢棄了,所以現在我們就只能自己模擬一個這樣的函式,自定義個屬性,預設為false,然後在componentWillUnmount生命週期中把這個屬性製為true,用法和isMounted一樣...

題外話

react除了廢棄這兩個api之外,還有一個很有意思的介面,叫batchedUpdates,這個api主要是控制setState機制的(是否立即生效),有興趣的同學可以檢視下資料.