React中constructor是唯一可以初始化state的地方,也可以把它理解成一個鉤子函數(shù),該函數(shù)最先執(zhí)行且只執(zhí)行一次。
更新狀態(tài)不要直接修改this.state。雖然狀態(tài)可以改變,但不會觸發(fā)組件的更新。
應(yīng)當(dāng)使用this.setState(),該方法接收兩種參數(shù):對象或函數(shù)。
- 對象:即想要修改的state
- 函數(shù):接收兩個函數(shù),第一個函數(shù)接受兩個參數(shù),第一個是當(dāng)前state,第二個是當(dāng)前props,該函數(shù)返回一個對象,和直接傳遞對象參數(shù)是一樣的,就是要修改的state;第二個函數(shù)參數(shù)是state改變后觸發(fā)的回調(diào)。
回到主題,setState可能是異步的。對此官方有這樣一段描述:setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState()a potential pitfall.
關(guān)鍵詞:batch、defer、may。
要探究setState為什么可能是異步的,先了解setState執(zhí)行后會發(fā)生什么?
事實上setState內(nèi)部執(zhí)行過程是很復(fù)雜的,大致過程包括更新state,創(chuàng)建新的VNode,再經(jīng)過diff算法比對差異,決定渲染哪一部分以及怎么渲染,最終形成最新的UI。這一過程包含組件的四個生命周期函數(shù)。
- shouleComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
需要注意的是如果子組件的數(shù)據(jù)依賴于父組件,還會執(zhí)行一個鉤子函數(shù)componentWillReceiveProps
。
假如setState是同步更新的,每更新一次,這個過程都要完整執(zhí)行一次,無疑會造成性能問題。事實上這些生命周期為純函數(shù),對性能還好,但是diff比較、更新DOM總消耗時間和性能吧。
此外為了批次和效能,多個setState有可能在執(zhí)行過程中還會被合并,所以setState延時異步更新是很合理的。
setState何時同步何時異步?
由React控制的事件處理程序,以及生命周期函數(shù)調(diào)用setState不會同步更新state 。
React控制之外的事件中調(diào)用setState是同步更新的。比如原生js綁定的事件,setTimeout/setInterval等。
大部分開發(fā)中用到的都是React封裝的事件,比如onChange、onClick、onTouchMove等,這些事件處理程序中的setState都是異步處理的。
看以下case:
constructor() {
this.state = {
count: 10
}
this.handleClickOne = this.handleClickOne.bind(this)
this.handleClickTwo = this.handleClickTwo.bind(this)
}
render() {
return (
<button onClick={this.hanldeClickOne}>clickOne</button>
<button onClick={this.hanldeClickTwo}>clickTwo</button>
<button id="btn">clickTwo</button>
)
}
handleClickOne() {
this.setState({ count: this.state.count + 1})
console.log(this.state.count)
}
輸出:10
由此可以看出該事件處理程序中的setState是異步更新state的。
componentDidMount() {
document.getElementById('btn').addEventListener('clcik', () => {
this.setState({ count: this.state.count + 1})
console.log(this.state.count)
})
}
輸出: 11
handleClickTwo() {
setTimeout(() => {
this.setState({ count: this.state.count + 1})
console.log(this.state.count)
}, 10)
}
輸出: 11
以上兩種方式繞過React,通過js的事件綁定程序 addEventListener 和使用setTimeout/setInterval 等 React 無法掌控的 APIs情況下,setState是同步更新state。
React是怎樣控制異步和同步的呢?
在 React 的 setState 函數(shù)實現(xiàn)中,會根據(jù)一個變量 isBatchingUpdates 判斷是直接更新 this.state 還是放到隊列中延時更新,而 isBatchingUpdates 默認(rèn)是 false,表示 setState 會同步更新 this.state;但是,有一個函數(shù) batchedUpdates,該函數(shù)會把 isBatchingUpdates 修改為 true,而當(dāng) React 在調(diào)用事件處理函數(shù)之前就會先調(diào)用這個 batchedUpdates將isBatchingUpdates修改為true,這樣由 React 控制的事件處理過程 setState 不會同步更新 this.state。
多個setState調(diào)用會合并處理
render() {
console.log('render')
}
hanldeClick() {
this.setState({ name: 'jack' })
this.setState({ age: 12 })
}
在hanldeClick處理程序中調(diào)用了兩次setState,但是render只執(zhí)行了一次。因為React會將多個this.setState產(chǎn)生的修改放在一個隊列里進(jìn)行批延時處理。
參數(shù)為函數(shù)的setState用法
先看以下case:
handleClick() {
this.setState({
count: this.state.count + 1
})
}
以上操作存在潛在的陷阱,不應(yīng)該依靠它們的值來計算下一個狀態(tài)。
handleClick() {
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
}
最終的結(jié)果只加了1
因為調(diào)用this.setState時,并沒有立即更改this.state,所以this.setState只是在反復(fù)設(shè)置同一個值而已,上面的代碼等同于這樣
handleClick() {
const count = this.state.count
this.setState({
count: count + 1
})
this.setState({
count: count + 1
})
this.setState({
count: count + 1
})
}
count相當(dāng)于一個快照,所以不管重復(fù)多少次,結(jié)果都是加1。
此外假如setState更新state后我希望做一些事情,而setState可能是異步的,那我怎么知道它什么時候執(zhí)行完成。所以setState提供了函數(shù)式用法,接收兩個函數(shù)參數(shù),第一個函數(shù)調(diào)用更新state,第二個函數(shù)是更新完之后的回調(diào)。
第一個函數(shù)接收先前的狀態(tài)作為第一個參數(shù),將此次更新被應(yīng)用時的props做為第二個參數(shù)。
increment(state, props) {
return {
count: state.count + 1
}
}
handleClick() {
this.setState(this.increment)
this.setState(this.increment)
this.setState(this.increment)
}
結(jié)果: 13
對于多次調(diào)用函數(shù)式setState的情況,React會保證調(diào)用每次increment時,state都已經(jīng)合并了之前的狀態(tài)修改結(jié)果。
也就是說,第一次調(diào)用this.setState(increment),傳給increment的state參數(shù)的count是10,第二調(diào)用是11,第三次調(diào)用是12,最終handleClick執(zhí)行完成后的結(jié)果就是this.state.count變成了13。
值得注意的是:在increment函數(shù)被調(diào)用時,this.state并沒有被改變,依然要等到render函數(shù)被重新執(zhí)行時(或者shouldComponentUpdate函數(shù)返回false之后)才被改變,因為render只執(zhí)行一次。
讓setState接受一個函數(shù)的API的設(shè)計是相當(dāng)棒的!不僅符合函數(shù)式編程的思想,讓開發(fā)者寫出沒有副作用的函數(shù),而且我們并不去修改組件狀態(tài),只是把要改變的狀態(tài)和結(jié)果返回給React,維護(hù)狀態(tài)的活完全交給React去做。正是把流程的控制權(quán)交給了React,所以React才能協(xié)調(diào)多個setState調(diào)用的關(guān)系。
在同一個事件處理程序中不要混用
case:
increment(state, props) {
return {
count: state.count + 1
}
}
handleClick() {
this.setState(this.increment)
this.setState({ count: this.state.count + 1 })
this.setState(this.increment)
}
結(jié)果: 12
第一次執(zhí)行setState,count為11,第二次執(zhí)行,this.state仍然是沒有更新的狀態(tài),所以this.state.count又打回了原形為10,加1以后變成11,最后再執(zhí)行setState,所以最終count的結(jié)果是12。(render依然只執(zhí)行一次)
setState的第二個回調(diào)參數(shù)會在更新state,重新觸發(fā)render后執(zhí)行。