React之setState調(diào)用unmount組件報警告

最近公司比較忙,所以更新進度比較慢,一是因為要梳理以前的代碼,二是因為趕進度!!!
這個問題也是機緣巧合碰到了,正趕上這周公司網(wǎng)絡(luò)無緣無故抽風(fēng)(很慢),然后那天在調(diào)試代碼的時候忽然發(fā)現(xiàn)瀏覽器報了一個這樣的Warning:

warn.png

其實從字面上也大概可以了解個大概,是由于setState只能更新掛載完成的組件或者正在掛載的組件,那么定位這個BUG的第一步當(dāng)然是先了解組件的生命周期

生命周期

之前我的一片文章其實已經(jīng)淺略的介紹了下生命周期,這里在補充下,facebook官方把生命周期分為三類,每類對應(yīng)的函數(shù)分別如下:

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

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

Unmounting:
componentWillUnmount()

其中這三大類對應(yīng)的函數(shù)中帶有Will的會在render之前調(diào)用,帶有Did的會在render之后調(diào)用,這里著重強調(diào)一下shouldComponentUpdate()這個生命周期,因為這個生命周期的返回值將會影響其他生命周期是否執(zhí)行,其中最值得關(guān)注的就是當(dāng)返回flase的時候不會觸發(fā)render(另外還有componentWillUpdate() componentDidUpdate()),所以這也就給了我們優(yōu)化項目的空間.由于題目的報錯是指我在Unmounting的時候調(diào)用了setState,所以我去排查項目,以為自己手滑寫錯了......但是結(jié)果我排查的時候發(fā)現(xiàn)我代碼中并沒有在componentWillUnmount()這個生命周期鉤子里面做setState的邏輯,于是好奇心之下我就在每個生命周期鉤子中都用了下setState,于是發(fā)現(xiàn)了一些有意思的事

有意思的事

code.png

console.png

這是在componetWillUpdate鉤子中寫了setState之后的效果,當(dāng)receive的時候就會無限觸發(fā)componetWillUpdate,這是為什么呢? 經(jīng)過程序媛的幫助 @眼角淺藍(鏈接在后面)http://www.reibang.com/u/8385c9b70d89,加上自己翻看源碼,大概理解如下:

React源碼中有這樣一個函數(shù):

performUpdateIfNecessary.png

這個函數(shù)負責(zé)update組件,我們可以看到當(dāng)this._pendingStateQueue !=null 時會觸發(fā)this.updateComponent, this.updateComponent函數(shù)的代碼如下:

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;
    }
  },

程序的最后幾行我們可以知道當(dāng)需要shouldUpdate的時候會執(zhí)行this._performComponentUpdate這個函數(shù),this._performComponentUpdate函數(shù)的源碼如下:


_performComponentUpdate.png

我們可以發(fā)現(xiàn)這個方法在次調(diào)用了componentWillUpdate方法,但是當(dāng)在componentWillUpdate周期中調(diào)用setState時就會觸發(fā)最上邊的performUpdateIfNecessary函數(shù),所以一直循環(huán)下去...
附一個生命周期執(zhí)行順序的圖:

lifeCycle.png

這里通過源碼在附一張生命周期是否可以寫setState函數(shù)的圖(這里的可以不可以不是指報錯,而是指是否有必要或者是避免出現(xiàn)無限循環(huán)):

image.png

es6語法中g(shù)etDefaultProps和getInitialState合并到構(gòu)造器constrator中.

回到主題

感覺扯著扯著有點遠了,那么這個bug怎么解決呢(我們暫且成為bug,其實不會影響程序運行),到底是怎么產(chǎn)生的呢,原來是因為我有一部分setState寫在了fetch的回調(diào)函數(shù)里,但fetch還沒結(jié)束時我已經(jīng)卸載了這個組件,當(dāng)請求結(jié)束后setState執(zhí)行的時候會發(fā)現(xiàn)這個組件已經(jīng)卸載了,所以才會報了這個warning...

解決辦法

1.把所有類似這種的請求放在項目中那個永遠不會卸載的組件里,比如最頂層組件,然后數(shù)據(jù)通過props分發(fā)傳遞,這也就是同樣的邏輯為什么通過redux的dispatch不會報warning,因為redux其實就一個最頂層的state,然后組件通過connect把值與props相關(guān)聯(lián)起來
2.第一種方法很麻煩,我現(xiàn)在采用的也是第二種,這里要提一下,其實react早就知道這個問題了,所以在最開始版本有這樣一個參數(shù)叫isMounted,大家可以打印一下組件里的this,就應(yīng)該能看的到,與這個一起的還有一個參數(shù)叫做replaceState,isMounted這個函數(shù)返回true和false,代表組件是否已經(jīng)卸載,有了這個參數(shù)之后我們就可以在fecth的回調(diào)函數(shù)里在添加一個判斷,只有組件未卸載才觸發(fā) setState,但是很不巧,這兩個api都被廢棄了,所以現(xiàn)在我們就只能自己模擬一個這樣的函數(shù),自定義個屬性,默認為false,然后在componentWillUnmount生命周期中把這個屬性制為true,用法和isMounted一樣...

題外話

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抽米,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闯传,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機茂缚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屋谭,“玉大人脚囊,你說我怎么就攤上這事⊥┐牛” “怎么了悔耘?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長我擂。 經(jīng)常有香客問我衬以,道長缓艳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任看峻,我火速辦了婚禮阶淘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘互妓。我一直安慰自己溪窒,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布冯勉。 她就那樣靜靜地躺著澈蚌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灼狰。 梳的紋絲不亂的頭發(fā)上宛瞄,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天,我揣著相機與錄音伏嗜,去河邊找鬼坛悉。 笑死,一個胖子當(dāng)著我的面吹牛承绸,可吹牛的內(nèi)容都是我干的裸影。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼军熏,長吁一口氣:“原來是場噩夢啊……” “哼轩猩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荡澎,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤均践,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后摩幔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彤委,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年或衡,在試婚紗的時候發(fā)現(xiàn)自己被綠了焦影。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡封断,死狀恐怖斯辰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情坡疼,我是刑警寧澤彬呻,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響闸氮,放射性物質(zhì)發(fā)生泄漏剪况。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一湖苞、第九天 我趴在偏房一處隱蔽的房頂上張望拯欧。 院中可真熱鬧,春花似錦财骨、人聲如沸镐作。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽该贾。三九已至,卻和暖如春捌臊,著一層夾襖步出監(jiān)牢的瞬間杨蛋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工理澎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逞力,地道東北人。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓糠爬,卻偏偏與公主長得像寇荧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子执隧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容