只要使用React的小伙伴,一定接触到setState这个API,地位不言而喻
但要使用好一定要抓住三个关键词:合并更新、同步异步、不可变值
咋一看,有些小伙伴可能云里雾里~
别急,我挨个解释
1.setState会合并更新
- 默认合并更新 先看一个例子🌰
import { Component } from 'react'
class App extends Component {
state= {
count: 1
}
handleAdd = () => {
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
{ this.state.count }
<button onClick={ this.handleAdd }>add</button>
</div>
)
}
}
这段代码,我们重复的setState让count不断加1
可我们会发现点击一次按钮仅加1
而不是我们想想加4
其实就是React的底层优化
他会把多个setState中的更新的对象合并一次更新
底层类似使用 Object.assign 对更新对象进行合并处理
// 等价于 { count: this.state.count + 1 }
Object.assign({
count: this.state.count + 1
}, {
count: this.state.count + 1
}, {
count: this.state.count + 1
}, {
count: this.state.count + 1
})
那如何避免不会被合并呢?
- 函数不会被合并
可以使用函数,函数不会被合并
// preState 更新之前的 state
// preProps 更新之前的 props
this.setState((preState, preProps) => {
// return 这次要更新的对象
})
来一个例子🌰
import { Component } from 'react'
class App extends Component {
state= {
count: 1
}
handleAdd = () => {
this.setState((preState) => {
console.log(preState) // {count: 1}
return ({
count: preState.count + 1
})
})
this.setState((preState) => {
console.log(preState) // {count: 2}
return ({
count: preState.count + 1
})
})
this.setState((preState) => {
console.log(preState) // {count: 3}
return ({
count: preState.count + 1
})
})
this.setState((preState) => {
console.log(preState) // {count: 4}
return ({
count: preState.count + 1
})
})
}
render() {
return (
<div>
{ this.state.count }
<button onClick={ this.handleAdd }>add</button>
</div>
)
}
}
2.setState异步同步都是有可能的
- 默认是异步 这是什么意思呢?
直接来个例子🌰
import { Component } from 'react'
class App extends Component {
state= {
count: 1
}
handleAdd = () => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count)
}
render() {
return (
<div>
{ this.state.count }
<button onClick={ this.handleAdd }>add</button>
</div>
)
}
}
效果图:
看着效果图,小伙伴其实可以一目了然发现
在 console.log 之前其实已经 setState
但获取的值却还是从 1 开始打印,而不是从 2 开始
也就是说 console.log 之前还未 setState
那此时 setState 便是异步更新
但我们可以使用 setState(updater, callback) 第二个参数(回调函数)
类似 Vue 中的 $nextTick
// 代码同上
// ...
handleAdd = () => {
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count) // 此时 count 等于2
})
}
// ...
当然你也可以在生命周期 componentDidUpdate 拿到全部完成更新的值
componentDidUpdate() {
console.log(this.state.count) // 此时 count 等于2
}
那何时同步?
- 同步情况有:使用异步函数、自定义事件
异步函数可以是setTimeout、setInterval、requestAnimationFrame
class App extends Component {
state= {
count: 1
}
handleAdd = () => {
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 此时 count 等于2
}, 0)
}
render() {
return (
<div>
{ this.state.count }
<button onClick={this.handleAdd}>add</button>
</div>
)
}
}
效果图:
从代码和上面的图,我们很容易看出
我们还是在 console.log 之前 setState
但是打印是从 2 开始的
那就可以说明此时setState 是同步更新的
类似自定义事件,附上例子🌰
import { Component } from 'react'
class App extends Component {
state= {
count: 1
}
componentDidMount() {
// 自定义事件
this.handleAdd = event => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 此时 count 等于2
}
const button = document.getElementById('button')
button.addEventListener('click', this.handleAdd);
}
componentWillUnmount() {
// 移除自定义事件,防止内存泄漏
const button = document.getElementById('button')
button.removeEventListener('click', this.handleAdd);
}
render() {
return (
<div>
{ this.state.count }
<button id='button'>add</button>
</div>
)
}
}
- 同步或异步的原理
一张很经典的图
(图源于网络)
这图什么意思呢?
其实说白就是 React 用 isBatchingUpdates 去判断 setState 后该如何处理
我们把点击事件拿出举例🌰
React 会在 setState 之前,isBatchingUpdates = true
到结束时才会让,isBatchingUpdates = false
此时,命中图中左侧判断:保存组件于 dirtyComponents 中
handleAdd = () => {
// 开始时处于BathingUpdates, isBatchingUpdates = true
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 此时 count 等于 1
// 结束时 isBatchingUpdates = false
}
而同步时呢?
同样,setState 之前,isBatchingUpdates = true
结束时,isBatchingUpdates = false
此时,isBatchingUpdates = false 才 setState 开始工作
命中图中的右侧的判断,马上开始一系列的更新数值
handleAdd = () => {
// ①开始时处于BathingUpdates, isBatchingUpdates = true
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // ③此时 count 等于2
}, 0)
// ②结束时 isBatchingUpdates = false
}
3.setState最好使用不可变值
使用不可变值,其实就是为了页面的性能优化
我们知道其实有些页面组件没有更新的必要
例如:父组件更新,子组件也会随着更新
但子组件其实没有数值的更新,所以没必要更新渲染
我们可以使用 shouldComponentUpdate 手动控制更新渲染
shouldComponentUpdate(nextProps, nextState) {
//...
// 如果 return false 代表组件不更新渲染
// 如果 return true 代表组件更新渲染
}
所以,我们就要用不可变值,两个前后不一样的props或是state对象进行比较
例如
// ...
// 传入数组list
shouldComponentUpdate(nextProps, nextState) {
// 引入 lodash 的 isEqual 比较数组值是否相等
if (isEqual(nextProps.list,this.props.list)) {
return false // 组件不更新
}
return false // 组件不更新
}
如果你不使用不可变值,那指向有可能是同一个对象
那么即时你使用shouldComponentUpdate,也会更新渲染
因为他们都是同一个对象
如果小伙伴不太明白~
那就看看我之前写的文章 如何理解react中的setState一定要用不可变值?说的很详细~
感谢阅读~