setState的异步和多次合并
在react官网的State&生命周期篇章中有详细介绍关于State以及setState的用法
tick() {
this.setState({
date: new Date()
});
}
最基础的用法就是上面这种例子,但是这种用法有一个问题
this.setState({
a: this.state.a + 1
})
this.setState({
a: this.state.a + 1
})
当我同时调用两次的时候,结果并不是我想象的 a + 2,而是 a + 1
这是因为在react调用的时候采用了异步更新的机制,在异步更新的过程中会将setState的执行全部放在一个queue中,在render执行之前合并多次更新
componentDidMount() {
this.setState({
a: this.state.a + 1
})
this.setState({
a: this.state.a + 1
})
console.log("this.state.a = " , this.state.a) // this.state.a = 1
}
render() {
console.log("render")
const { a } = this.state;
return (<div id="a">
{a}
</div>)
}
上面的代码 “render”只会执行两次,第一次是初始化,第二次是两个setState合并后的执行
由此可以看出,setState的一些特性
- 异步执行
- 可能会自动合并多次setState
这是react为了性能考虑而做的优化,但是这也有一个问题,假设我需要获得一个执行出 a + 2的结果时就需要换一种方式实现了
拒绝setState的合并
官网方式
this.setState((state:any) => {
return {
a: state.a + 1
}
})
this.setState((state:any) => {
return {
a: state.a + 1
}
})
这种方式的处理是官网推荐的处理方式,强烈建议使用,虽然多了几行代码,但是真的很优秀,不要嫌写回调函数麻烦
下面看另一种方式
setTimeout(() => {
console.log(this.state)
this.setState({
a:4
})
this.setState({
a:2
})
},0)
这种方式也能实现setState的拒绝合并,但是两者有一个很重要的区别,我们观察一下在render方法中的调用
官网方式的
执行两次render
setTimeOut的
执行了三次render,发现区别了吗?这就是为什么强烈推荐回调函数写法的原因
setState执行的过程
这里推荐一个很好懂的setState实现过程可以帮助你理解一下setState的运行过程
首先setState执行的时候是将当前的事件存入队列中,随后可以利用js的时间循环机制来确定什么时候把时间队列清空
首先定义一个往队列里添加内容的方法
const queue = [];
function enqueueSetState( stateChange, component ) {
// 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加
if ( queue.length === 0 ) {
// 按照事件循环机制,fn将在同步序列执行完之后首先执行
Promise.resolve().then( fn );
}
queue.push( {
stateChange,
component
} );
}
清空队列的方法
function flush() {
let item;
// 遍历
while( item = setStateQueue.shift() ) {
const { stateChange, component } = item;
// 如果没有prevState,则将当前的state作为初始的prevState
if ( !component.prevState ) {
component.prevState = Object.assign( {}, component.state );
}
// 如果stateChange是一个方法,也就是setState的第二种形式
if ( typeof stateChange === 'function' ) {
Object.assign( component.state, stateChange( component.prevState, component.props ) );
} else {
// 如果stateChange是一个对象,则直接合并到setState中
Object.assign( component.state, stateChange );
}
// 注意这里,component.state是原值并没有改变,所以每次合并Object.assign(component.state, stateChange);而不是Object.assign(component.prevState, stateChange);
component.prevState = component.state;
}
}
当我们执行setState方法时
setState( stateChange ) {
enqueueSetState( stateChange, this );
}
从这时候开始,flush就会被添加进异步队列里等待同步执行完成后执行,这期间所有的stateChange都会被放进队列中等待flush执行的时候合并,具体的实现还是看setState实现过程
看完这里你应该也就理解了为什么上面用官方推荐的方式不会多执行一次render了