一.幾個開發中經常會遇到的問題
以下幾個問題是我們在實際開發中經常會遇到的場景,下面用幾個簡單的示例程式碼來還原一下。
1.setState是同步還是非同步的,為什麼有的時候不能立即拿到更新結果而有的時候可以?
1.1 鉤子函式和React合成事件中的setState
現在有兩個元件
componentDidMount() {
console.log('parent componentDidMount');
}
render() {
return (
<div>
<SetState2></SetState2>
<SetState></SetState>
</div>
);
}
複製程式碼
元件內部放入同樣的程式碼,並在Setstate1
中的componentDidMount
中放入一段同步延時程式碼,列印延時時間:
componentWillUpdate() {
console.log('componentWillUpdate');
}
componentDidUpdate() {
console.log('componentDidUpdate');
}
componentDidMount() {
console.log('SetState呼叫setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index);
console.log('SetState呼叫setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index);
}
複製程式碼
下面是執行結果:
說明:
- 1.呼叫
setState
不會立即更新 - 2.所有元件使用的是同一套更新機制,當所有元件
didmount
後,父元件didmount
,然後執行更新 - 3.更新時會把每個元件的更新合併,每個元件只會觸發一次更新的生命週期。
1.2 非同步函式和原生事件中的setstate
?
在setTimeout
中呼叫setState
(例子和在瀏覽器原生事件以及介面回撥中執行效果相同)
componentDidMount() {
setTimeout(() => {
console.log('呼叫setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index);
console.log('呼叫setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index);
}, 0);
}
複製程式碼
執行結果:
說明:
- 1.在父元件
didmount
後執行 - 2.呼叫
setState
同步更新
2.為什麼有時連續兩次setState
只有一次生效?
分別執行以下程式碼:
componentDidMount() {
this.setState({ index: this.state.index + 1 }, () => {
console.log(this.state.index);
})
this.setState({ index: this.state.index + 1 }, () => {
console.log(this.state.index);
})
}
複製程式碼
componentDidMount() {
this.setState((preState) => ({ index: preState.index + 1 }), () => {
console.log(this.state.index);
})
this.setState(preState => ({ index: preState.index + 1 }), () => {
console.log(this.state.index);
})
}
複製程式碼
執行結果:
1
1
複製程式碼
2
2
複製程式碼
說明:
- 1.直接傳遞物件的
setstate
會被合併成一次 - 2.使用函式傳遞
state
不會被合併
二.setState執行過程
由於原始碼比較複雜,就不貼在這裡了,有興趣的可以去github
上clone
一份然後按照下面的流程圖去走一遍。
1.流程圖
partialState
:setState
傳入的第一個引數,物件或函式_pendingStateQueue
:當前元件等待執行更新的state
佇列isBatchingUpdates
:react用於標識當前是否處於批量更新狀態,所有元件公用dirtyComponent
:當前所有處於待更新狀態的元件佇列transcation
:react的事務機制,在被事務呼叫的方法外包裝n個waper
物件,並一次執行:waper.init
、被呼叫方法、waper.close
FLUSH_BATCHED_UPDATES
:用於執行更新的waper
,只有一個close
方法
2.執行過程
對照上面流程圖的文字說明,大概可分為以下幾步:
- 1.將setState傳入的
partialState
引數儲存在當前元件例項的state暫存佇列中。 - 2.判斷當前React是否處於批量更新狀態,如果是,將當前元件加入待更新的元件佇列中。
- 3.如果未處於批量更新狀態,將批量更新狀態標識設定為true,用事務再次呼叫前一步方法,保證當前元件加入到了待更新元件佇列中。
- 4.呼叫事務的
waper
方法,遍歷待更新元件佇列依次執行更新。 - 5.執行生命週期
componentWillReceiveProps
。 - 6.將元件的state暫存佇列中的
state
進行合併,獲得最終要更新的state物件,並將佇列置為空。 - 7.執行生命週期
componentShouldUpdate
,根據返回值判斷是否要繼續更新。 - 8.執行生命週期
componentWillUpdate
。 - 9.執行真正的更新,
render
。 - 10.執行生命週期
componentDidUpdate
。
三.總結
1.鉤子函式和合成事件中:
在react
的生命週期和合成事件中,react
仍然處於他的更新機制中,這時isBranchUpdate
為true。
按照上述過程,這時無論呼叫多少次setState
,都會不會執行更新,而是將要更新的state
存入_pendingStateQueue
,將要更新的元件存入dirtyComponent
。
當上一次更新機制執行完畢,以生命週期為例,所有元件,即最頂層元件didmount
後會將isBranchUpdate
設定為false。這時將執行之前累積的setState
。
2.非同步函式和原生事件中
由執行機制看,setState
本身並不是非同步的,而是如果在呼叫setState
時,如果react
正處於更新過程,當前更新會被暫存,等上一次更新執行後在執行,這個過程給人一種非同步的假象。
在生命週期,根據JS的非同步機制,會將非同步函式先暫存,等所有同步程式碼執行完畢後在執行,這時上一次更新過程已經執行完畢,isBranchUpdate
被設定為false,根據上面的流程,這時再呼叫setState
即可立即執行更新,拿到更新結果。
3.partialState
合併機制
我們看下流程中_processPendingState
的程式碼,這個函式是用來合併state
暫存佇列的,最後返回一個合併後的state
。
_processPendingState: function (props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null;
if (!queue) {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = _assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;
},
複製程式碼
我們只需要關注下面這段程式碼:
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
複製程式碼
如果傳入的是物件,很明顯會被合併成一次:
Object.assign(
nextState,
{index: state.index+ 1},
{index: state.index+ 1}
)
複製程式碼
如果傳入的是函式,函式的引數preState是前一次合併後的結果,所以計算結果是準確的。
4.componentDidMount
呼叫setstate
在componentDidMount()中,你 可以立即呼叫setState()。它將會觸發一次額外的渲染,但是它將在瀏覽器重新整理螢幕之前發生。這保證了在此情況下即使render()將會呼叫兩次,使用者也不會看到中間狀態。謹慎使用這一模式,因為它常導致效能問題。在大多數情況下,你可以 在constructor()中使用賦值初始狀態來代替。然而,有些情況下必須這樣,比如像模態框和工具提示框。這時,你需要先測量這些DOM節點,才能渲染依賴尺寸或者位置的某些東西。
以上是官方文件的說明,不推薦直接在componentDidMount
直接呼叫setState
,由上面的分析:componentDidMount
本身處於一次更新中,我們又呼叫了一次setState
,就會在未來再進行一次render
,造成不必要的效能浪費,大多數情況可以設定初始值來搞定。
當然在componentDidMount
我們可以呼叫介面,再回撥中去修改state
,這是正確的做法。
當state初始值依賴dom屬性時,在componentDidMount
中setState
是無法避免的。
5.componentWillUpdate
componentDidUpdate
這兩個生命週期中不能呼叫setState
。
由上面的流程圖很容易發現,在它們裡面呼叫setState
會造成死迴圈,導致程式崩潰。
6.推薦使用方式
在呼叫setState
時使用函式傳遞state
值,在回撥函式中獲取最新更新後的state
。