幾個月前,我開始中止使用React的 setState 。我並非再也不須要組件狀態,並且再也不用React來管理個人組件狀態。html
setState對於新手來講不是很友好,即便是有經驗的React程序員在使用setState時,也很容易出bug,好比:react
Bug產生的緣由是忘記了React的state是異步的;從日誌打印延遲能夠看出來。git
React的官方文檔已將把使用setState可能會出現的全部問題都總結了:程序員
注意:github
永遠不要直接修改
this.state
,須要經過調用this.setState
方法來替換你修改後的值。把this.state
當作不可變數據來處理。api
setState()
不會立刻去改變this.state
,而是會排隊等待處理,因此當你調用setState()
後訪問this.state
,有可能會返回舊的state
。bash當你調用
setState()
時,沒法保證是同步執行的,由於爲了保證性能可能會被批處理。數據結構
setState()
老是會觸發render()
進行從新渲染,除非在shouldComponentUpdata()
控制了渲染邏輯。若是用了可變數據結構以及在shouldComponentUpdata()
中並無控制渲染邏輯,調用setState()
將不會觸發從新渲染渲染。app
總的來講,使用setState
會帶來三個問題:異步
setState
是異步的許多開發人員起初並無意識到這一點,當你設置了新的state
,卻發現沒有變化,這是setState
最詭異的地方,由於setState
調用的時候看起來並非異步。好比下面的代碼:
class Select extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
selection: props.values[0]
};
}
render() {
return (
<ul onKeyDown={this.onKeyDown} tabIndex={0}>
{this.props.values.map(value =>
<li
className={value === this.state.selection ? 'selected' : ''}
key={value}
onClick={() => this.onSelect(value)}
>
{value}
</li>
)}
</ul>
)
}
onSelect(value) {
this.setState({
selection: value
})
this.fireOnSelect()
}
onKeyDown = (e) => {
const {values} = this.props
const idx = values.indexOf(this.state.selection)
if (e.keyCode === 38 && idx > 0) { /* up */
this.setState({
selection: values[idx - 1]
})
} else if (e.keyCode === 40 && idx < values.length -1) { /* down */
this.setState({
selection: values[idx + 1]
})
}
this.fireOnSelect()
}
fireOnSelect() {
if (typeof this.props.onSelect === "function")
this.props.onSelect(this.state.selection) /* not what you expected..*/
}
}
ReactDOM.render(
<Select
values={["State.", "Should.", "Be.", "Synchronous."]}
onSelect={value => console.log(value)}
/>,
document.getElementById("app")
)複製代碼
乍一看沒什麼問題,可是這個select
組件有一個bug,上面的git圖已經很好的證實了。onSelect()
方法觸發時老是獲得前一個state.selection
的值,由於setState
尚未完成,fireOnSelect
就被調用了。我認爲應該把setState
從新命名爲scheduleState
或者要求傳入回調。
這個bug很容易修復,棘手的地方在於你很難發現它。
setState
引發沒有必要的渲染setState
的第二個問題在於它老是會觸發從新渲染,不少時候這種渲染是沒有必要的。你能夠經過printWasted(React提供的性能工具)來檢測它會在何時從新渲染。從三個方面來粗略的講爲何從新渲染有時候是沒有必要的:
當新的state
和舊的state
是同樣的。能夠經過shouldComponentUpdate
來解決,也能夠用純渲染庫來解決。
只有某些時候state
的改變才和渲染有關係。
第三,某些時候state
和視圖層一點關係都沒有,好比用來管理事件的監聽器,定時器等相關的state
。
setState
不可能管理全部組件的狀態接着上面最後那點說,不是全部的組件狀態都須要經過setState
來儲存和更新。大多數複雜的組件一般須要管理定時器循環,接口請求,事件等等。若是用setState
來管理,不只僅會引發沒有必要渲染,並且會形成死循環。
代碼以下:
import {observable} from "mobx"
import {observer} from "mobx-react"
@observer class Select extends React.Component {
@observable selection = null; /* MobX managed instance state */
constructor(props, context) {
super(props, context)
this.selection = props.values[0]
}
render() {
return (
<ul onKeyDown={this.onKeyDown} tabIndex={0}>
{this.props.values.map(value =>
<li
className={value === this.selection ? 'selected' : ''}
key={value}
onClick={() => this.onSelect(value)}
>
{value}
</li>
)}
</ul>
)
}
onSelect(value) {
this.selection = value
this.fireOnSelect()
}
onKeyDown = (e) => {
const {values} = this.props
const idx = values.indexOf(this.selection)
if (e.keyCode === 38 && idx > 0) { /* up */
this.selection = values[idx - 1]
} else if (e.keyCode === 40 && idx < values.length -1) { /* down */
this.selection = values[idx + 1]
}
this.fireOnSelect()
}
fireOnSelect() {
if (typeof this.props.onSelect === "function")
this.props.onSelect(this.selection) /* solved! */
}
}
ReactDOM.render(
<Select
values={["State.", "Should.", "Be.", "Synchronous."]}
onSelect={value => console.log(value)}
/>,
document.getElementById("app")
)複製代碼
效果以下:
用同步的組件狀態的機制沒有出現意想不到的bug。
上面的代碼片斷不只簡潔美觀,MobX
還解決了setState
的所有問題:
改變state
立刻就反映到了組件state
上。這讓代碼邏輯和代碼重用變得更簡單。你不用去擔憂state
是否更新了。
MobX
在運行時候肯定哪些state
與視圖層關聯,暫時與視圖層無關的State
不會引發從新渲染,直到再次關聯。
因此可渲染和不可渲染狀態是統一處理的。
此外,直接修改state
對象的低級錯誤不能再犯了。還有,不要擔憂是否還能用shouldComponentUpdate 和或者PureRenderMixin方法,MobX
也很好的處理了。最後,你也許會想,我如何等到setState
處理完成呢,你可使用compentDidUpdate
生命週期。
我已經中止使用React來管理組件狀態了,而是用MobX
代替。如今React
成了「正真」的視圖層:)。
如下是實現的Dome: