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í)行安疗。