一、setState 這個磨人的小妖精!
作為一名入職前基本沒有接觸過React的小菜鳥,在接手的第一個練手專案中,很快就遇到了許多React初學者都會遇到的問題-setState。
let promiseArr = [];
this.setState({
topStoryIds: res.data
})
for (let i = 0; i < 30; i++)
{
promiseArr.push(axios.get(`https://hacker-news.firebaseio.com/v0/item/` this.state.newStoryIds[i] .json?print=pretty`));
}
複製程式碼
最初的設想是先通過this.setState設定state中的資料,然後再取用this.state。當然,結果不出所料的悲劇了。。苦思不得,不停的打斷點,找原因,一度懷疑自己腦子瓦特了。。
就在我即將準備砸鍵盤的時候,突然想起來之前看文件的時候,好像提到過setState有個啥“非同步更新”的東東,如一道閃電劃過我的腦海。
二、setState是個啥
setState,是React官方推出的更新state的用法。通過呼叫setState,React能夠知道state發生了變化,並呼叫render方法將變化展現到檢視。
this.setState({
count: this.state.count + 1,
});
複製程式碼
瞭解了基本概念及用法,我們來看一下setState的注意點:
- setState通過引發一次元件的更新過程來引發重新繪製;
- 多次setState函式呼叫產生的效果會合並;
- setState不會立刻改變React元件中state的值。
setState通過引發一次元件的更新過程來引發重新繪製
setState呼叫引起的React的更新生命週期函式4個函式(比修改prop引發的生命週期少一個componentWillReceiveProps函式),這4個函式依次被呼叫。
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
那麼state何時被更新呢,我們用一個小栗子來探索一哈。
handleClick = () => {
this.setState({ count: this.state + 1, });
}
shouldComponentUpdate() {
console.log(`shouldComponentUpdate`,this.state.count);
return true;
}
componentWillUpdate() {
console.log(`componentWillUpdate`,this.state.count);
}
render() {
console.log(`render`,this.state.count);
}
componentDidUpdate() {
console.log(`componentDidUpdate`,this.state.count);
}
複製程式碼
對應的控制檯資訊如下:
shouldComponentUpdate 0
componentWillUpdate 0
render [object Object]1
componentDidUpdate [object Object]1
複製程式碼
由此可知,state一直到render函式執行的時候,才會被更新,在這之前,state一直保持為更新前的狀態。(或者,當shouldComponentUpdate函式返回false,這時候更新過程就被中斷了,render函式也不會被呼叫了,這時候React不會放棄掉對this.state的更新的,所以雖然不呼叫render,依然會更新this.state。)
多次setState函式呼叫產生的效果會合並
Talk is cheap, show me your code!
handleClick = () => {
this.setState({ count: this.state.count + 1, });
console.log(`第一次加一`, this.state.count);
this.setState({ count: this.state.count + 1, });
console.log(`第二次加一`, this.state.count);
}
render() {
console.log(`render加一`, this.state.count);
return (
<div className="App">
<button onClick={this.handleClick}>count + 1</button>
</div>
);
}
複製程式碼
對應的渲染得到的檢視即一個button:
單擊button後,控制檯的輸出結果如下:
第一次加一 0
第二次加一 0
render加一 [object Object]1
複製程式碼
如上所示,我們在handleClick中進行了兩次setState操作,但對應的render卻只執行了一次,說明了React將兩次setState合併為了一次,進行merge後統一更新。
其實想想也很容易理解,若每次setState都觸發一次更新行為的話,那麼將造成多麼大的效能浪費。所以,從效能角度考慮,setState“多次setState函式呼叫產生的效果會合並”這一特性是合理而有必要的。
但是,相信善於觀察的你一定會有一個疑問:為什麼上例中render列印的this.state.count不是2,而是1呢?明明handleClick中進行了兩次加一操作啊!這也是我最開始看到這個栗子時候的一個疑問。
三、setState的非同步更新機制
setState不會立刻改變React元件中state的值
其實,問題的答案在前面已經給出了,即“state一直到render函式執行的時候,才會被更新”。
回到上面的栗子中去:
handleClick = () => {
this.setState({ count: this.state.count + 1, });
console.log(`第一次加一`, this.state.count);
this.setState({ count: this.state.count + 1, });
console.log(`第二次加一`, this.state.count);
}
複製程式碼
表面上看,handleClick對this.state.count執行了兩次加一操作,最終的this.state.count應該等於2,但是,因為React的非同步更新機制,導致this.state.count直到render函式執行前依然未得到更新,即上面的兩次this.state.count + 1操作是冗餘,與下面的程式碼等價:
handleClick = () => {
const count = this.state.count;
this.setState({ count: count + 1, });
console.log(`第一次加一`, this.state.count);
this.setState({ count: count + 1, });
console.log(`第二次加一`, this.state.count);
}
複製程式碼
顯而易見,這樣的更新是不會得到預想的結果的。此時的this.state.count只是對state的一個“快照”,不管執行多少次加一操作,其最終結果都只相當於一次。
四、setState會同步更新嗎?
setState函式存在的一個重要意義就是它可以驅動檢視的更新。如果僅僅想要改變state,我們可以直接對this.state物件進行操作:
handleClick = () => {
this.state.count++;
console.log(`this.state.count: `, this.state.count);
}
render() {
console.log(`觸發更新`);
...
}
複製程式碼
控制檯輸出:
this.state.count: 1
複製程式碼
可以看到,state值確實進行了操作,但是render函式卻沒有得到執行。這樣的更新操作是沒有意義的。
既然如此,那麼setState會進行同步更新嗎?答案是肯定的。
在React中,如果是由React引發的事件處理(比如通過onClick引發的事件處理),呼叫setState不會同步更新this.state,除此之外的setState呼叫會同步執行this.state。所謂“除此之外”,指的是繞過React通過addEventListener直接新增的事件處理函式,還有通過setTimeout/setInterval產生的非同步呼叫。
在React的setState函式實現中,會根據一個變數isBatchingUpdates判斷是直接更新this.state還是放到佇列中回頭再說,而isBatchingUpdates預設是false,也就表示setState會同步更新this.state,但是,有一個函式batchedUpdates,這個函式會把isBatchingUpdates修改為true,而當React在呼叫事件處理函式之前就會呼叫這個batchedUpdates,造成的後果,就是由React控制的事件處理過程setState不會同步更新this.state。
但是,setState的同步更新會導致嚴重的效能問題,我們在實際開發過程中應儘量避免使用。
五、耍個流氓?
既然直接操作this.state會導致無法觸發re-render過程,而setState又具有非同步更新這一讓人又愛又恨的特性,我們何不二者合二為一?
handleClick = () => {
this.state.count++;
this.state.count++;
this.state.count++;
console.log(`this.state.count: `, this.state.count);
this.setState({});
}
render() {
console.log(`觸發更新了呢!`);
console.log(`render`,this.state.count);
...
}
複製程式碼
控制檯資訊:
this.state.count: 3
觸發更新了呢!
render 3
複製程式碼
結果居然通過了!!!這樣的操作,既避免了非同步更新導致的同時多次更新state時的無效問題,又解決了直接操作this.state時不會觸發更新的問題,豈不是兩全其美?
這個問題仁者見仁,不同人眼中也許會有不同的答案。但我相信,React的作者設計這個框架的初衷肯定不在於此。