最近公司比較忙,所以更新進度比較慢,一是因為要梳理以前的代碼,二是因為趕進度!!!
這個問題也是機緣巧合碰到了,正趕上這周公司網絡無緣無故抽風(很慢),然后那天在調試代碼的時候忽然發現瀏覽器報了一個這樣的Warning:
其實從字面上也大概可以了解個大概,是由于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,于是發現了一些有意思的事
有意思的事
這是在componetWillUpdate鉤子中寫了setState之后的效果,當receive的時候就會無限觸發componetWillUpdate,這是為什么呢? 經過程序媛的幫助 @眼角淺藍(鏈接在后面)http://www.lxweimin.com/u/8385c9b70d89,加上自己翻看源碼,大概理解如下:
React源碼中有這樣一個函數:
這個函數負責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函數的源碼如下:
我們可以發現這個方法在次調用了componentWillUpdate方法,但是當在componentWillUpdate周期中調用setState時就會觸發最上邊的performUpdateIfNecessary函數,所以一直循環下去...
附一個生命周期執行順序的圖:
這里通過源碼在附一張生命周期是否可以寫setState函數的圖(這里的可以不可以不是指報錯,而是指是否有必要或者是避免出現無限循環):
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機制的(是否立即生效),有興趣的同學可以查看下資料.