setState 算是 React 里被使用的最高频的 api,但你真的了解 setState 吗?比如下面这段代码,你能清楚的知道输出什么吗?
import { Component } from 'react'
export class stateDemo extends Component {
state = {
count: 0
}
componentDidMount() {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count)
this.setState({ count: this.state.count + 1 })
console.log(this.state.count)
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count)
this.setState({ count: this.state.count + 1 })
console.log(this.state.count)
}, 0)
}
render() {
return null
}
}
export default stateDemo
要彻底弄懂这道题,就不得不聊 setState 的异步更新,另外输出结果也要看当前处于哪种模式下。 我们先从 setState 的用法说起,以便全面掌握该 api。
1、为什么需要 setState
虽然我们一直在用 setState ,可有没想过为什么 React 里会有该 api ? React 是通过管理状态来实现对组件的管理,即 UI = f(state) f 就是我们的代码,最主要的就是 this.setState ,调用该函数后 React 会使用更新的 state 重新渲染此组件及其子组件,即达到了 UI 层的变更。
2、什么是 setState
setState 是 React 官方提供的更新 state 的方法,通过调用 setState ,React 会使用最新的 state 值,并调用 render 方法将变化展现到视图。 在 React v16.3 版本之前,调用 setState 方法会依次触发以下生命周期函数
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
那么 state 在哪个生命周期里会更新为最新的值?
import React, { Component } from 'react'
export default class stateDemo2 extends Component {
state = {
count: 0
}
shouldComponentUpdate() {
console.info('shouldComponentUpdate', this.state.count)
return true
}
componentWillUpdate() {
console.info('componentWillUpdate', this.state.count)
}
increase = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
console.info('render', this.state.count)
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.increase}>累加</button>
</div>
)
}
componentDidUpdate() {
console.info('componentDidUpdate', this.state.count)
}
}
可以看到,直到 render 执行时,state 的值才变更为最新的值,在些之前,state 一直保持为更新前的状态。
在 React v16.3 版本之后,调用 setState 方法会依次触发以下生命周期函数
确切的说,应该是 v16.4 版本之后,v16.3 版本 setState 并不会触发 getDerivedStateFromProps 函数
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
那么 state 在哪个生命周期里会更新为最新的值?
import React, { Component } from 'react'
export default class stateDemo3 extends Component {
state = {
count: 0
}
static getDerivedStateFromProps(props, state) {
console.info('getDerivedStateFromProps', state.count)
return { ...state }
}
shouldComponentUpdate() {
console.info('shouldComponentUpdate', this.state.count)
return true
}
increase = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
console.info('render', this.state.count)
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.increase}>累加</button>
</div>
)
}
getSnapshotBeforeUpdate() {
console.info('getSnapshotBeforeUpdate', this.state.count)
return null
}
componentDidUpdate() {
console.info('componentDidUpdate', this.state.count)
}
}
可以看到新增的两个生命周期函数 getDerivedStateFromProps 与 getSnapshotBeforeUpdate 获取到的 state 都是新值
3、setState 用法
3.1、setState(stateChange[, callback])
第一个参数是一个对象,会将传入的对象浅层合并到 ;第二个参数是个可选的回调函数
例如,调整购物车商品数:
this.setState({quantity: 2})
在回调函数参数里,可以获取到最新的 state 值,但推荐使用 componentDidUpdate
3.2、setState(updater, [callback])
第一个参数是个函数,(state, props) => stateChange,第二个参数同上是个可选的回调函数
例如:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
updater 函数中接收的 state 和 props 都保证为最新。updater 的返回值会与 state 进行浅合并。
4、state 的不可变性
我们要严格遵行 state 是不可变的原则,即不可以直接修改 state 变量,例如底下的做法就是不可取的
this.state.count ++
this.state.count ++
this.setState({})
这样是实现了同步更改 state 的目的,但违背了 state 是不可变的原则
4.1、基本数据类型
this.setState({
count: 1,
name: 'zhangsan',
flag: true
})
4.2、对象类型
使用 ES6 的 Object.assign 或解构赋值
this.setState({
person:{...this.state.person,age:22}
})
4.3、数组类型
this.setState((prevState) => {
return {
hobbys:[...prevState.hobbys,'writing']
}
})
this.setState({
hobbys: this.state.hobbys.slice(0, 2)
})
- 插入选项
使用 slice 克隆一份,然后用 splice 插入选项
this.setState((prevState) => {
let currentState = prevState.hobbys.slice()
currentState.splice(1, 0, 'basketball')
return {
hobbys: currentState
}
})
this.setState({
hobbys: this.state.hobbys.filter((item) => item.length < 5)
})
注意,不能直接使用 push pop splice shift unshift 等,因为这些方法都是在原数组的基础上修改,这样违反不可变值
5、setState 到底是异步还是同步?
先给出答案:
- 在
legacy 模式中,即通过 ReactDOM.render(<App />, rootNode) 创建的,在合成事件和生命周期函数里是异步的,在原生事件和 setTimeout 、promise 等异步函数是同步的 - 在
blocking 模式中,即通过 ReactDOM.createBlockingRoot(rootNode).render(<App />) 创建的,任何场景下 setState 都是异步的 - 在
concurrent 模式中,即通过 ReactDOM.createRoot(rootNode).render(<App />) 创建的,任何场景下 setState 都是异步的
模式的说明详看官网 但由于后两种模式目前处于实验阶段,所以我们先重点分析下 legacy 模式,后面源码分析时,会说明下为什么其他两个模式都是异步的。
5.1 合成事件和生命周期函数里是异步的
import React, { Component } from 'react'
export default class stateDemo5 extends Component {
state = {
count:0
}
componentDidMount() {
this.setState({
count:this.state.count+1
})
console.info("didMount count:",this.state.count)
}
handleChangeCount = () => {
this.setState({
count:this.state.count+1
})
console.info("update count:",this.state.count)
}
render() {
return (
<div>
{this.state.count}
<button onClick={this.handleChangeCount}>更改</button>
</div>
)
}
}
可以看到在 componentDidMount 生命周期函数与 handleChangeCount 合成事件里,setState 之后,获取到的 state 的值是旧值。
5.1.1、setState 合并处理
采用这种设置 state 方式,也会出现合并的现象:
import React, { Component } from 'react'
export default class stateDemo6 extends Component {
state = {
count:0
}
handleChangeCount = () => {
this.setState({
count:this.state.count+1
},() => {
console.info("update count:",this.state.count)
})
this.setState({
count:this.state.count+1
},() => {
console.info("update count:",this.state.count)
})
this.setState({
count:this.state.count+1
},() => {
console.info("update count:",this.state.count)
})
}
render() {
return (
<div>
{this.state.count}
<button onClick={this.handleChangeCount}>更改</button>
</div>
)
}
}
输出控制台信息如下:
update count: 1
update count: 1
update count: 1
本质上等同于 Object.assign :
Object.assign(state,
{count: state.count + 1},
{count: state.count + 1},
{count: state.count + 1}
)
即后面的对象会覆盖前面的,所以只有最后的 setState 才是有效 那么要怎么弄才不会合并呢? 将 setState 的第一个参数设置为函数形式:
import React, { Component } from 'react'
export default class stateDemo7 extends Component {
state = {
count:0
}
handleChangeCount = () => {
this.setState(prevState => {
return {
count:prevState.count+1
}
},() => {
console.info("update count:",this.state.count)
})
this.setState(prevState => {
return {
count:prevState.count+1
}
},() => {
console.info("update count:",this.state.count)
})
this.setState(prevState => {
return {
count:prevState.count+1
}
},() => {
console.info("update count:",this.state.count)
})
}
render() {
return (
<div>
{this.state.count}
<button onClick={this.handleChangeCount}>更改</button>
</div>
)
}
}
输出控制台信息如下:
update count: 3
update count: 3
update count: 3
函数式 setState 工作机制类似于:
[
{increment: 1},
{increment: 1},
{increment: 1}
].reduce((prevState, props) => ({
count: prevState.count + props.increment
}), {count: 0})
5.2 在原生事件和 setTimeout 里是同步的
import React, { Component } from 'react'
export default class stateDemo8 extends Component {
state = {
count:0
}
componentDidMount() {
document.querySelector("#change").addEventListener("click", () => {
this.setState({
count: this.state.count + 1,
});
console.log("update count1:", this.state.count);
});
}
handleChangeCount = () => {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
});
console.log("update count2:", this.state.count);
}, 0);
}
render() {
return (
<div>
<p>{this.state.count}</p>
<button id="change">更改1</button>
<button onClick={this.handleChangeCount}>更改2</button>
</div>
)
}
}
可以看到原生的事件(通过 addEventListener 绑定的),或者 setTimeout 等异步方式更改的 state 是同步的。
|