你真的瞭解setState()嗎?
React 中 setState()詳細解讀
對於 setState() 相信夥伴們都用過,它是 React 官方推薦用來更新元件 state 的 API,但是對於 setState() 你真的瞭解嗎?且待我慢慢詳聊一番。
setState() 官方用法指南
語法1: setState(updater[, callback])
updater:函式型別,返回一個更新後的 state 中的狀態物件,它會和 state 進行淺合併。
callback: 可選,回撥函式。
語法2: setState(stateChange[, callback])
setState: 物件型別,會將傳入的物件淺層合併到新的 state 中。
callback:可選,回撥函式。
對於這兩種形式,不同的是第一個引數選擇問題,可以選擇一個函式返回一個新的state物件,亦可以直接選擇一個物件應用於狀態更新,那麼啥時候選擇函式型別的引數,什麼時候選擇物件型別的呢?這裡可以總結兩句話:
當前狀態更新無需依賴之前的state狀態時,選擇物件型別引數
當前更新狀態依賴之前的狀態時,選擇函式型別引數
example:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>setState詳解</title> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://unpkg.com/[email protected]/babel.min.js"></script> </head> <body> <div id="app"></div> <script type="text/babel"> class A extends React.Component { state = { count: 0 } update1 = () => { this.setState({count: this.state.count+1}) } update2 = () => { this.setState(state => ({ count: state.count+1 })) } update3 = () => { this.setState({ count: 8 }) } render () { return ( <div> <h1>{this.state.count}</h1> <button onClick={this.update1} style={{marginRight: 15}}>測試1</button><button style={{marginRight: 15}} onClick={this.update2}>測試2</button><button onClick={this.update3}>測試3</button> </div> ) } } ReactDOM.render( <A/>, document.getElementById('app') ) </script> </body> </html>
這個例子中,我們通過點選按鈕測試1或測試2來改變元件 A 的 count 狀態值,因為每次修改狀態都是在原先基礎上加 1, 所以在setState 中適合選擇函式型別引數,即 update2 寫法推薦。
點選 測試3 按鈕會直接將count 值修改為 固定值 8,這無需依賴上一次count狀態值,所以在setState 中適合選擇物件型別引數,即 update3 寫法推薦。
setState() 更新狀態一定是非同步的嗎?
我們知道setState() 會觸發元件render() 函式,重新渲染元件將更新後的內容顯示在檢視上,那麼在 setState() 之後我們立馬就能獲取到最新的state值嗎?
這裡涉及到一個 setState() 是非同步更新還是同步更新的問題?
結論:
在React相關的回撥函式中setState() 是非同步更新
不在React 相關的回撥中setState() 是同步更新
React 相關的回撥包括:元件的生命週期鉤子,React 元件事件監聽回撥。
React不相關的回撥包括常見的:setTimeout(), Promise()等。
我們還是可以拿之前的按鈕點選例項來測試。
example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>setState詳解</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
class A extends React.Component {
state = {
count: 0
}
update1 = () => {
this.setState({count: this.state.count+1})
console.log(this.state.count)
}
update2 = () => {
setTimeout(() => {
this.setState(state => ({
count: state.count+1
}))
console.log(this.state.count)
})
}
update3 = () => {
Promise.resolve().then(value => {
this.setState({
count: 8
})
console.log(this.state.count)
})
}
componentWillMount () {
this.setState(state => ({
count: state.count+1
}))
console.log(this.state.count)
}
render () {
console.log('render()', this.state.count)
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.update1} style={{marginRight: 15}}>測試1</button><button style={{marginRight: 15}} onClick={this.update2}>測試2</button><button onClick={this.update3}>測試3</button>
</div>
)
}
}
ReactDOM.render(
<A/>,
document.getElementById('app')
)
</script>
</body>
</html>
我們在 React 事件監聽回撥 update1 和 元件生命週期 componentWillMount() 鉤子裡面分別在setState()之後列印最新的 state 值,發現打印出來的還是修改之前的state,但是頁面已經更新為最新狀態,看圖:
採用同樣的方法我們可以觀察在 update2 的setTimeout() 和 update3 的 Promise() 回撥中,setState() 後列印的是最新的state值,而且這個列印會在setState() 觸發元件重新render() 之後。經過測試,恰好驗證了我們的結論是正確的,在React 相關的回撥中setState()是非同步更新狀態,在不相關的回撥中 setState() 是同步更新狀態。
setState() 非同步更新狀態時,如何獲取最新的狀態值?
這個問題其實是針對當setState() 非同步更新狀態之後,怎麼立馬獲取到最新的狀態值,也就是上面例子我們說的在update1() 和componentWillMount()中怎麼打印出最新的state值。
答案其實非常簡單,也就是我們說到的setState()傳參的第二個callback() 引數。setState() 的第二個回撥會在更新狀態之後,元件重新render() 之後呼叫,也就是這裡面我們可以獲取到最新的狀態值。
程式碼:
...
update1 = () => {
this.setState({count: this.state.count+1}, () => {
console.log(this.state.count)
})
}
componentWillMount () {
this.setState(state => ({
count: state.count+1
}), () => {
console.log(this.state.count)
})
}
這樣,我們同樣可以在update1 和 componentWillMount() 中 打印出最新的state值。
遇到重複多次呼叫setState(),React如何處理?
這裡我們討論的前提當然是setState() 非同步更新狀態時候,因為同步更新,我們呼叫幾次 setState(),就會觸發幾次 render鉤子,當然也會實時分別打印出更新後的狀態值。
結論:
這裡分兩種情況討論:
當setState() 傳物件型別引數,React會合並重復多次的呼叫setState(),觸發一次render。
當setState() 傳函式型別引數,React會依次多次的呼叫setState(),觸發一次render。
可以看到,我們多次重複呼叫setState(),不管是傳參是何種型別。React都只會呼叫一次 render,重新渲染元件。
我們可以同樣以按鈕點選例項來測試我們結論。
example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>setState詳解</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
class A extends React.Component {
state = {
count: 0
}
update1 = () => {
// this.setState({count: this.state.count+1}, () => {
// console.log(this.state.count)
// })
// this.setState({count: this.state.count+1}, () => {
// console.log(this.state.count)
// })
// this.setState({count: this.state.count+1}, () => {
// console.log(this.state.count)
// })
this.setState((state) => ({
count: state.count+1
}), () => {
console.log(this.state.count)
})
this.setState((state) => ({
count: state.count+1
}), () => {
console.log(this.state.count)
})
this.setState((state) => ({
count: state.count+1
}), () => {
console.log(this.state.count)
})
}
update2 = () => {
setTimeout(() => {
this.setState(state => ({
count: state.count+1
}))
console.log(this.state.count)
this.setState(state => ({
count: state.count+1
}))
console.log(this.state.count)
this.setState(state => ({
count: state.count+1
}))
console.log(this.state.count)
})
}
update3 = () => {
Promise.resolve().then(value => {
this.setState({
count: 8
})
console.log(this.state.count)
})
}
componentWillMount () {
this.setState(state => ({
count: state.count+1
}))
console.log(this.state.count)
}
render () {
console.log('render()', this.state.count)
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.update1} style={{marginRight: 15}}>測試1</button><button style={{marginRight: 15}} onClick={this.update2}>測試2</button><button onClick={this.update3}>測試3</button>
</div>
)
}
}
ReactDOM.render(
<A/>,
document.getElementById('app')
)
</script>
</body>
</html>
當點選測試按鈕2,因為setState() 是同步更新狀態,可以發現元件進行了多次render呼叫,分別依次打印出更新後的狀態值,這個很簡單。
我們點選測試按鈕1,分別對傳給setState()引數不同進行了測試,發現當傳參是物件型別時候,React會合並重復setState()呼叫,也就是隻更新一次state狀態,傳函式型別引數時候,則分別進行了計算更新。
無論以哪種方式傳參重複呼叫 setState() ,React 都只會進行一次render 呼叫,這也是效能優化的一部分,防止多次重複渲染帶來的效能問題。
其實官網推薦我們使用setState()時候,第一個引數傳函式型別引數,因為函式引數中接收的 state 和 props 都保證為最新