setState 的原理機制解析
我們本章節主要是要來分析一下React
中常見的setState
方法,熟悉React
的小夥伴應該都知道,該方法通常用於改變組件狀態並用新的state
去更新組件。但是,這個方法在很多地方的表現總是與我們的預期不符,先來看幾個案例。
常見案例
class Root extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
let that = this;
that.setState({ count: that.state.count + 1 });
console.log(that.state.count); // 打印出 0
that.setState({ count: that.state.count + 1 });
console.log(that.state.count); // 打印出 0
setTimeout(function(){
that.setState({ count: that.state.count + 1 });
console.log(that.state.count); // 打印出 2
}, 0);
setTimeout(function(){
that.setState({ count: that.state.count + 1 });
console.log(that.state.count); // 打印出 3
}, 0);
}
render() {
return (
<h1>{this.state.count}</h1>
)
}
}
這個案例的結果確實讓人非常意外,打印出 0
,0
,2
,3
。爲什麼沒有按照我們預期的1
,2
,3
,4
往下進行這樣的打印方式呢?我們接着往下看:
在瞭解setState
之前,我們先來簡單瞭解下 React 一個包裝結構: Transaction
:
事務 (Transaction
):
它呢,是 React 中的一個調用結構,用於包裝一個方法,結構爲: initialize
- perform(thatthod)
- close
。通過事務,可以統一管理一個方法的開始與結束;處於事務流中,表示進程正在執行一些操作;
設置狀態值 setState
:
在 React
中setState
用於修改狀態,更新視圖。它具有以下特點:
異步與同步: setState
並不是單純的異步或同步,這其實與調用時的環境相關:
- 在 合成事件 和 生命週期鉤子(除
componentDidUpdate
) 中,setState是"異步"的;
原因:
因爲在setState
的實現中,有一個判斷: 當更新策略正在事務流的執行中時,該組件更新會被推入dirtyComponents
隊列中等待執行;否則,開始執行batchedUpdates
隊列更新;
- 在生命週期鉤子調用中,更新策略都處於更新之前,組件仍處於事務流中,而
componentDidUpdate
是在更新之後,此時組件已經不在事務流中了,因此則會同步執行; - 在合成事件中,
React
是基於 事務流完成的事件委託機制 實現,也是處於事務流中;
- 問題: 無法在
setState
後馬上從this.state
上獲取更新後的值。- 解決: 如果需要馬上同步去獲取新值,
setState
其實是可以傳入第二個參數的。setState(updater, callback)
,在回調中即可獲取最新值;
- 解決: 如果需要馬上同步去獲取新值,
setState({
index: 1
}}, function(){
console.log(this.state.index);
})
- 在鉤子函數中體現:
componentDidUpdate(){
console.log(this.state.index);
}
-
在 原生事件 和
setTithatout
中,setState
是同步的,可以馬上獲取更新後的值;- 原因: 原生事件是瀏覽器本身的實現,與事務流無關,自然是同步;而
setTithatout
是放置於定時器線程中延後執行,此時事務流已結束,因此也是同步;
- 原因: 原生事件是瀏覽器本身的實現,與事務流無關,自然是同步;而
-
批量更新: 在 合成事件 和 生命週期鉤子 中,
setState
更新隊列時,存儲的是 合併狀態(Object.assign
)。因此前面設置的key
值會被後面所覆蓋,最終只會執行一次更新;如下圖所示:
- 函數式: 由於
Fiber
及 合併 的問題,官方推薦可以傳入 函數 的形式。setState(fn)
,在fn中返回新的state
對象即可,例如this.setState((state, props) => newState)
;使用函數式,可以用於避免setState
的批量更新的邏輯,傳入的函數將會被 順序調用;
class Com extends Component{
constructor(props){
super(props);
this.state = { index: 0 };
this.add = this.add.bind(this);
}
add(){
this.setState(prevState => {
return {index: prevState.index + 1};
});
this.setState(prevState => {
return {index: prevState.index + 1};
});
}
}
注意事項:
-
setState
合併,在 合成事件 和 生命週期鉤子 中多次連續調用會被優化爲一次; -
當組件已被銷燬,如果再次調用
setState
,React
會報錯警告,通常有兩種解決辦法:- 將數據掛載到外部,通過
props
傳入,如放到Redux
或 父級中; - 在組件內部維護一個狀態量
(isUnmounted)
,componentWillUnmount
中標記爲true
,在setState
前進行判斷;
- 將數據掛載到外部,通過