最佳实践
- 把所有的
setState
当做异步的。- 永远不要信任
setState
调用之后的状态(可能未更新)。- 如果要使用改变后的状态,需要使用回调函数(setState 的第2个参数)。
- 如果新的状态,需要使用之前的状态参与计算,则使用函数的方式改变状态(
setState
的第1个参数改为函数)
1,异步更新状态
this.setState()
改变状态后,会触发 render
函数执行,但不是立即执行。
而状态的改变是异步还是同步,取决于执行 setState()
的方法是否通过事件调用。如果是事件调用,则是异步的。
状态的改变,才会触发
render
执行。
下面的代码中,状态更新就是异步的。所以第1次点击时输出:0,render
class MyClassComp extends Component {
state = {
num: 0,
};
handleClick = () => {
this.setState({
num: this.state.num + 1,
});
console.log(this.state.num);
};
render() {
console.log("render");
return (
<>
<div>{this.state.num}</div>
<button onClick={this.handleClick}>加1</button>
</>
);
}
}
第2个参数
setState()
的第2个参数是函数,可获取更改后的状态。该函数在 render 之后执行(状态改变,才能获取新的状态)。
更改 handleClick
后,第1次点击输出:0,render,第2个参数 1
handleClick = () => {
this.setState({
num: this.state.num + 1,
}, () => {
console.log('第2个参数',this.state.num);
});
console.log(this.state.num);
};
2,多个 setState
1,问题
更改 handleClick
如下,第1次点击后输出:0,render
。页面显示 1
handleClick = () => {
this.setState({
num: this.state.num + 1,
});
this.setState({
num: this.state.num + 1,
});
this.setState({
num: this.state.num + 1,
});
console.log(this.state.num);
};
原因:因为状态更改是异步的,所以上面的代码相当于:
handleClick = () => {
this.setState({
num: 0 + 1,
});
this.setState({
num: 0 + 1,
});
this.setState({
num: 0 + 1,
});
console.log(this.state.num);
};
2,配合第2个参数解决
更改 handleClick
如下,第1次点击时输出:0,3次render
,页面显示 3
上面说了,第2个函数参数会在状态更新后执行,所以执行顺序:
状态更新–> render --> 状态更新–> render --> 状态更新–> render
handleClick = () => {
this.setState({
num: this.state.num + 1,
}, () => {
this.setState({
num: this.state.num + 1,
}, () => {
this.setState({
num: this.state.num + 1,
})
})
});
console.log(this.state.num);
};
3,第1个参数是函数
使用场景:如果遇到某个事件中,需要同步调用多次setState
,则可使用函数的方式得到最新状态。
这种情况下,会将每个 setState
的函数参数放到一个队列中,按顺序执行。队列执行完毕后,再更新真正的 state,再执行 render
(只执行了1次)。
注意是对异步 setState 的处理。同步的更新并不会合并!render 函数会执行多次。
为什么会合并?因为 React 对事件处理函数处理的思路时,元素的事件处理,可能会操作比较多的东西,如果不加限制,会影响性能。比如一个点击事件的逻辑中会触发多个自定义方法,每个方法中又会更改不同的状态。所以会将他们合并为一次修改,做成异步的,最终统一更新,执行一次 render 函数。
而不在HTML元素的事件中,不会遇到复杂的处理,比如计时器中。
在队列中的函数,可以获取上一个函数更新后的 state
。换句话说,作为参数的 state 是可以被信任的(最新的)。
更改 handleClick
如下,第一次点击时输出:0,1,2,3,render
,页面显示 3
handleClick = () => {
this.setState((curState) => {
console.log(1);
return {
num: curState.num + 1,
};
});
this.setState((curState) => {
console.log(2);
return {
num: curState.num + 1,
};
});
this.setState((curState) => {
console.log(3);
return {
num: curState.num + 1,
};
});
console.log(this.state.num);
};
注意,第2个参数依旧会在 render 执行后执行。
修改第1个 setState
如下,第一次点击的输出:0,1,2,3,render,x
,
this.setState((curState) => {
console.log(1);
return {
num: curState.num + 1,
};
}, () => {
console.log('x')
});
4,同步更新状态举例
虽然是在点击事件中,但因为嵌套了计时器,所以可看做同步
setState
。
举例1:1s 后输出:render,1
handleClick = () => {
setTimeout(() => {
this.setState({
num: this.state.num + 1,
});
console.log(this.state.num);
}, 1000);
};
举例2:1s 后输出:3次render,3
handleClick = () => {
setTimeout(() => {
this.setState({
num: this.state.num + 1,
});
this.setState({
num: this.state.num + 1,
});
this.setState({
num: this.state.num + 1,
});
console.log(this.state.num);
}, 1000);
};
举例3,异步函数不会阻塞 render 执行,但异步函数之后的 setState
算做同步。
const delay = (duration = 1000) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
};
handleClick = async () => {
this.setState({
num: this.state.num + 1,
});
// 会同步执行 render
await delay();
// num 已经是修改后的
this.setState({
num: this.state.num + 1,
});
console.log(this.state.num);
};
第一次点击是的输出:render,(等待1s)render,2
这里,第2个 render 比 2 先输出,就是因为异步函数之后,算做同步
setState
了。
以上。