setState 同步更新
我們在上文中提及,為了提高性能React將setState設置為批次更新,即是異步操作函數,并不能以順序控制流的方式設置某些事件,我們也不能依賴于this.state來計算未來狀態。典型的譬如我們希望在從服務端抓取數據并且渲染到界面之后,再隱藏加載進度條或者外部加載提示:
componentDidMount() {
fetch('https://example.com')
.then((res) => res.json())
.then(
(something) => {
this.setState({ something });
StatusBar.setNetworkActivityIndicatorVisible(false);
}
);
}
因為setState函數并不會阻塞等待狀態更新完畢,因此setNetworkActivityIndicatorVisible有可能先于數據渲染完畢就執行。我們可以選擇在componentWillUpdate與componentDidUpdate這兩個生命周期的回調函數中執行setNetworkActivityIndicatorVisible,但是會讓代碼變得破碎,可讀性也不好。實際上在項目開發中我們更頻繁遇見此類問題的場景是以某個變量控制元素可見性:
this.setState({showForm : !this.state.showForm});
我們預期的效果是每次事件觸發后改變表單的可見性,但是在大型應用程序中如果事件的觸發速度快于setState的更新速度,那么我們的值計算完全就是錯的。本節就是討論兩種方式來保證setState的同步更新。
完成回調
setState函數的第二個參數允許傳入回調函數,在狀態更新完畢后進行調用,譬如:
this.setState({
load: !this.state.load,
count: this.state.count + 1
}, () => {
console.log(this.state.count);
console.log('加載完成')
});
這里的回調函數用法相信大家很熟悉,就是JavaScript異步編程相關知識,我們可以引入Promise來封裝setState:
setStateAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}
setStateAsync返回的是Promise對象,在調用時我們可以使用Async/Await語法來優化代碼風格:
async componentDidMount() {
StatusBar.setNetworkActivityIndicatorVisible(true)
const res = await fetch('https://api.ipify.org?format=json')
const {ip} = await res.json()
await this.setStateAsync({ipAddress: ip})
StatusBar.setNetworkActivityIndicatorVisible(false)
}
這里我們就可以保證在setState渲染完畢之后調用外部狀態欄將網絡請求狀態修改為已結束,整個組件的完整定義為:
class AwesomeProject extends Component {
state = {}
setStateAsync(state) {
...
}
async componentDidMount() {
...
}
render() {
return (
My IP is {this.state.ipAddress || 'Unknown'}
);
}
}
該組件的執行效果如下所示:
傳入狀態計算函數
除了使用回調函數的方式監聽狀態更新結果之外,React還允許我們傳入某個狀態計算函數而不是對象來作為第一個參數。狀態計算函數能夠為我們提供可信賴的組件的State與Props值,即會自動地將我們的狀態更新操作添加到隊列中并等待前面的更新完畢后傳入最新的狀態值:
this.setState(function(prevState, props){
return {showForm: !prevState.showForm}
});
這里我們以簡單的計數器為例,我們希望用戶點擊按鈕之后將計數值連加兩次,基本的組件為:
class Counter extends React.Component{
constructor(props){
super(props);
this.state = {count : 0}
this.incrementCount = this.incrementCount.bind(this)
}
incrementCount(){
...
}
render(){
return
Increment
}
}
直觀的寫法我們可以連續調用兩次setState函數,這邊的用法可能看起來有點怪異,不過更多的是為了說明異步更新帶來的數據不可預測問題。
incrementCount(){
this.setState({count : this.state.count + 1})
this.setState({count : this.state.count + 1})
}
上述代碼的效果是每次點擊之后計數值只會加1,實際上第二個setState并沒有等待第一個setState執行完畢就開始執行了,因此其依賴的當前計數值完全是錯的。我們當然可以使用上文提及的setStateAsync來進行同步控制,不過這里我們使用狀態計算函數來保證同步性:
incrementCount(){
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
}
這里的第二個setState傳入的prevState值就是第一個setState執行完畢之后的計數值,也順利保證了連續自增兩次。