React 組件狀態(tài)
React 把組件看成是一個狀態(tài)機(State Machines)脯宿。通過與用戶的交互亩进,實現(xiàn)不同狀態(tài),然后渲染 UI膨俐,讓用戶界面和數(shù)據(jù)保持一致勇皇。
setState()
則是 React 組件狀態(tài)更新的入口,調(diào)用 setState()
會對一個組件的 state 對象安排一次更新焚刺。當 state 改變了敛摘,該組件就會重新渲染。
一次組件更新的過程其實很復雜乳愉,包括 React 生命周期鉤子的執(zhí)行兄淫、虛擬DOM 的創(chuàng)建屯远、diff 對比、真實DOM 的創(chuàng)建 等等捕虽。
setState 批量/合并 更新
那么問題來了慨丐,是不是每次調(diào)用 setState()
都會觸發(fā)組件重新渲染呢?
如果不確定的話泄私,我們就來做個試驗驗證一下房揭。由于 React 組件每次渲染都會調(diào)用 componentDidUpdate
生命周期方法,我們可以在這個方法中打個日志:
export default class App extends Component {
constructor (p) {
super(p)
this.state = {
name: "peak",
age: 10
}
}
componentDidUpdate() {
// 組件更新時觸發(fā)
console.log("組件更新")
}
handleClick = () => {
this.setState({
name: "peak1"
})
this.setState({
age: 11
})
}
render () {
return (
<View>
<Text>姓名:{this.state.name}晌端,年齡:{this.state.age}</Text>
<TouchableOpacity onPress={this.handleClick}>
<Text>更新組件</Text>
</TouchableOpacity>
</View>
)
}
}
可以發(fā)現(xiàn)點擊按鈕觸發(fā) setState()
時捅暴,componentDidUpdate
只走了一次,也就是說咧纠,并不是每次調(diào)用 setState()
都會觸發(fā)組件重新渲染蓬痒。
在這個案例中,多個 setState()
被合并成了一次更新惧盹,這就是 setState()
的批量更新乳幸,或者稱為 合并更新。
setState()
的合并更新還有另一種表達方式钧椰,就是我們常說的 異步粹断,異步的 setState()
表現(xiàn)為:調(diào)用 setState()
之后無法立刻獲取到最新的 this.state
值。通過下面的日志可以直觀的發(fā)現(xiàn)這一點:
handleClick = () => {
this.setState({
name: "peak1"
})
console.log("name=",this.state.name) // 打拥障肌:peak
this.setState({
age: 11
})
console.log("age=",this.state.age) // 打悠柯瘛:10
}
What?還有同步的 setState诊沪?
實際上合并更新是 React 的一種優(yōu)化策略养筒,目的在于避免頻繁的觸發(fā)組件重新渲染,但是這個優(yōu)化是有條件的端姚,并不是所有的 setState()
都能被合并晕粪。
下面是 setState 的偽代碼:
setState(newState) {
if (this. isBatchingUpdates) {
this.updateQueue.push(newState)
return
}
// 下面是真正的更新: 修改 this.state,dom-diff, lifeCycle...
...
}
setState 會通過一個變量來判斷當前狀態(tài)變更是否能夠被合并渐裸,如果可以合并巫湘,就會將本次更新緩存起來,等到后面來一次性更新昏鹃;如果不可以合并尚氛,就會立即更新組件。
意思就是洞渤,當 isBatchingUpdates 為 false 時阅嘶,setState()
會立即觸發(fā)組件渲染,同時 this.state
的值也會相應(yīng)的變化载迄,我們能夠立即拿到最新的 this.state
值讯柔。此時的 setState()
表現(xiàn)并非是 異步抡蛙,而是 同步 的。
從這里可以看出磷杏,setState(x)
并不等于 this.state = x
溜畅。修改 this.state
的時機被 React 封裝了一層,只有當真正去渲染組件的時候 this.state
的值才會變化极祸。這就造成了我們看到的 同步 和 異步 的現(xiàn)象慈格。
有的人可能會說,同步 很好啊遥金,我能夠立即獲取到最新的 this.state
值浴捆,很直觀。有這種想法的人忽略了一個重要的問題稿械,就是 在同步場景中选泻,每次調(diào)用 setState()
變更狀態(tài)都會觸發(fā)組件重新渲染,導致性能下降美莫。 正因為如此页眯,所以 React 才引入合并更新來避免組件頻繁的重新渲染。
那么問題又來了厢呵,既然 同步 更新會導致性能下降窝撵,那為什么 React 不直接全都用 異步 呢,這樣就能合并更新了襟铭。為了找到答案碌奉,我們接著往下看。
setState 什么時候是同步寒砖,什么時候是異步赐劣?
React 的更新是基于 Transaction(事務(wù))的,Transacation 就是給目標函數(shù)包裹一下哩都,加上前置和后置的 hook魁兼,在開始執(zhí)行之前先執(zhí)行 initialize hook
,結(jié)束之后再執(zhí)行 close hook
漠嵌,這樣搭配上 isBatchingUpdates
這樣的布爾標志位就可以實現(xiàn)目標函數(shù)調(diào)用棧內(nèi)的多次 setState()
全部入 pending 隊列璃赡,結(jié)束后統(tǒng)一 apply 了。
這里的 目標函數(shù) 指的是 React 控制的函數(shù)献雅,這樣的函數(shù)主要有兩類:React 合成事件 和 生命周期鉤子; 而 setTimeout 這樣的異步方法是脫離事務(wù)的塌计,React 管控不到挺身,所以就沒法對其中的 setState()
進行合并了。
我們結(jié)合下面的 Demo 來具體分析一下:
class App extends React.Component {
handleClick = () => {
this.setState({x: 1})
this.setState({x: 2})
this.setState({x: 3})
setTimeout(() => {
this.setState({x: 4})
this.setState({x: 5})
this.setState({x: 6})
}, 0)
}
render() {
return (<View>
<TouchableOpacity onPress={this.handleClick}>
<Text>更新組件</Text>
</TouchableOpacity>
</View>
}
}
1锌仅、handleClick 是 React 合成事件的回調(diào)章钾,React 有控制權(quán)墙贱,在開始執(zhí)行該函數(shù)的時候會將 isBatchingUpdates
設(shè)置為 true,所以 x 為 1贱傀、2惨撇、3 是合并的;
2府寒、開始執(zhí)行 setTimeout魁衙,這里會將 setTimeout 的回調(diào)函數(shù)加入了事件循環(huán)的宏任務(wù)中,等待主線程完成所有任務(wù)后來進行調(diào)度株搔;
3剖淀、handleClick 結(jié)束之后 isBatchingUpdates
被重新設(shè)置為 false;
4纤房、此時主線程的函數(shù)已出棧纵隔,開始執(zhí)行 setTimeout 的回調(diào)函數(shù),由于 isBatchingUpdates
的值已經(jīng)變?yōu)榱?false炮姨,所以 x 為 4捌刮、5、6 沒有被合并更新舒岸,每一次的 setState()
都是同步執(zhí)行的绅作;
5、總共觸發(fā)了 4 次組件渲染吁津,其中有 2 次是冗余的棚蓄。
總結(jié)為如下:
- 由 React 控制的事件處理程序、生命周期鉤子中的
setState()
是異步的碍脏; - React 控制之外的事件中調(diào)用
setState()
是同步的梭依。比如網(wǎng)絡(luò)請求、setTimeout典尾、setInterval役拴、Promise 等; -
setState()
的 “異步” 并不是說內(nèi)部由異步代碼實現(xiàn)钾埂,其實本身執(zhí)行的過程和代碼都是同步的河闰,只是合成事件和鉤子函數(shù)的調(diào)用順序在更新之前,導致在合成事件和鉤子函數(shù)中沒法立馬拿到更新后的值褥紫,形成了所謂的 “異步”姜性。
由此可以看出 React 對于 setState()
的同步更新其實是迫于無奈,是 React 無法控制的髓考。React 當然想目標函數(shù)中的 setState()
都是異步更新的部念,這樣性能也是最好的,能夠避免組件頻繁的更新渲染,但是條件不允許儡炼,React 辦不到妓湘。
那我們能不能在寫代碼的時候規(guī)避同步的 setState()
調(diào)用呢?這是不可能的乌询,除非你的程序非常簡單且不需要跟后臺進行通信榜贴,只要你的程序要請求網(wǎng)絡(luò)接口,那么就會產(chǎn)生同步的 setState()
調(diào)用妹田。那難道就沒有辦法對同步的 setState()
進行優(yōu)化唬党,讓其合并更新嗎?
setState 手動合并(同步轉(zhuǎn)異步)
React 合成事件秆麸、生命周期鉤子 都在 React 的控制范圍內(nèi)初嘹,所以它能夠?qū)⑺麄?strong>自動加入 React 事務(wù)中,讓其中的 setState()
合并更新沮趣。對于 React 無法控制的目標函數(shù)屯烦,React 其實也有提供手動加入事務(wù)的 API,就是 unstable_batchedUpdates
房铭。
我們將上面 setTimeout 中的代碼做一下調(diào)整:
class App extends React.Component {
handleClick = () => {
this.setState({x: 1})
this.setState({x: 2})
this.setState({x: 3})
setTimeout(() => {
// 手動將目標函數(shù)加入 React 事務(wù)中驻龟,讓其合并更新
unstable_batchedUpdates(() => {
this.setState({x: 4})
this.setState({x: 5})
this.setState({x: 6})
})
}, 0)
}
render() {
return (<View>
<TouchableOpacity onPress={this.handleClick}>
<Text>更新組件</Text>
</TouchableOpacity>
</View>
}
}
x 為 1、2缸匪、3 在一個可控的目標函數(shù)中翁狐,是合并更新的;而 x 為 4凌蔬、5露懒、6 使用了 unstable_batchedUpdates
加入事務(wù),也是合并更新的砂心⌒复剩總共有 2 次更新,相較于之前的 4 次減少了 2 次辩诞。
unstable_batchedUpdates
API 的原理如下:
function unstable_batchedUpdates(fn) {
this.isBatchingUpdates = true
fn()
this.isBatchingUpdates = false
const finalState = ... //通過this.updateQueue合并出finalState
this.setState(finaleState)
}
這個 API 在 React 和 React Native 中的引入方式有所不同:
-
react 中通過
react-dom
進行引入
import { unstable_batchedUpdates } from "react-dom";
-
react-native 中則直接從
react-native
庫中引入
import { unstable_batchedUpdates } from "react-native";
React 的這個 API 確實能夠?qū)⑼降?code>setState() 轉(zhuǎn)換為異步來進行合并更新坎弯,避免組件頻繁渲染。
但是根據(jù)其前綴 unstable
也可以看出來译暂,這個 API 不是穩(wěn)定的抠忘。實際上這是 React 實驗性的 API 之一,并沒有全力推給到開發(fā)者去使用外永,所以如果不是特別影響性能崎脉,可以不用強制用這個 API 去合并 setState()
。
setState 的隱藏 API
我們在使用 setState 時用的最多就是給它傳一個對象伯顶,像下面這樣:
this.setState({count: 1})
如果 setState 中的 count 需要依賴之前的值荧嵌,你會怎么處理:
1呛踊、第一種方法:使用 setState 的第二個參數(shù)
this.setState({ count: this.state.count + 1 }, () => {
// 依賴當前 count 的值
this.setState({ count: this.state.count + 1 })
})
setState()
的第二個參數(shù)接收一個函數(shù),這個函數(shù)會在當前 setState()
更新完組件之后觸發(fā)啦撮。這種寫法有兩個缺陷:
- 破壞了 React 合并更新的優(yōu)化,會導致組件渲染兩次汪厨;
- 同時這種寫法會導致嵌套太深赃春,很不美觀。
2劫乱、第二種方法:將 setState 轉(zhuǎn)為同步執(zhí)行
setTimeout(() => {
this.setState({ count: this.state. count + 1 })
this.setState({ count: this.state. count + 1 })
})
通過 setTimeout 能夠?qū)?setState()
轉(zhuǎn)為同步代碼织中,這樣就能夠立即獲取到最新的 this.state
值。這個方法不存在嵌套衷戈,但是和上面一樣狭吼,會導致組件渲染兩次。
3殖妇、終極方法:使用函數(shù)式的 setState
setState
其實有一個隱藏 API刁笙,第一個參數(shù)除了能夠接收對象之外,還能夠接收一個函數(shù)谦趣。這個函數(shù)接收先前的 state 作為參數(shù)疲吸,同時返回本次需要變更的 state,如下:
this.setState((state) => {
return { count: state.count + 1 }
})
this.setState((state) => {
return { count: state.count + 1 }
})
函數(shù)式的 setState()
能夠保證第一個函數(shù)參數(shù)中的 state 是合并了之前所有狀態(tài)的前鹅,這樣后面的函數(shù)就能拿到前面函數(shù)執(zhí)行的結(jié)果摘悴。但是這個過程中并不會改變 this.state
的值,意思就是會等函數(shù)執(zhí)行完后才去進行渲染更新舰绘,所以組件只會渲染一次蹂喻,沒有破壞 React 合并更新的優(yōu)化。
在同一個目標函數(shù)中不要混用函數(shù)式和對象式這兩種API
// 1
this.setState((state) => {
return { count: state.count + 1 }
})
// 2
this.setState({ count: this.state.count + 1 })
// 3
this.setState((state) => {
return { count: state.count + 1 }
})
1捂寿、假設(shè)一開始的 state.count 為 10
2口四、第一次執(zhí)行函數(shù)式 setState
,count 為 11
3者蠕、第二次執(zhí)行對象式 setState
窃祝,this.state
仍然是沒有更新的狀態(tài),所以 this.state.count
還是 10踱侣,加 1 以后又變回了 11
4粪小、最后再執(zhí)行函數(shù)式 setState
,回調(diào)函數(shù)中的 state.count
的值是第二步中的到的 11抡句,這里再加 1探膊,所以最終 count 的結(jié)果是 12。
可以發(fā)現(xiàn)第二個對象式 setState
將第一個函數(shù)式設(shè)置的 count 抹掉了待榔,正確的做法是都調(diào)整為函數(shù)式的 setState
逞壁,不然可能就會造成上面的問題流济。所以要避免函數(shù)式和對象式的 setState
混用,不然自己可能都會搞迷糊腌闯。
總結(jié)
在使用 React 作為開發(fā)框架的項目中绳瘟,setState()
應(yīng)該是我們接觸使用最多的 API,大家都習以為常的認為 setState()
是異步更新的姿骏,實際上有很多同步更新的場景被大家所忽略糖声,從而忽視了對于 setState 也能進行性能優(yōu)化的場景。
文章提到的 setState 性能優(yōu)化主要包含兩方面:
- 適時地考慮使用
unstable_batchedUpdates
來手動合并更新分瘦,解決 React 無法自動合并更新的場景蘸泻。由于這個 API 不穩(wěn)定,所以未來可能會失效嘲玫,但目前在 RN 0.64.2 及之前的版本中驗證還是可以使用的悦施,暫時可以不用擔心; - 使用函數(shù)式的
setState()
來更新那些依賴于當前的 state 的 state去团。
本文為原創(chuàng)抡诞,轉(zhuǎn)載請注明出處