1. setState 何时同步,何时异步更新?
1.1 由 React 控制的事件处理程序,及生命周期函数中 setState 异步更新,多个 setState 可能会被合并更新。
组件 1 state 中有两个变量,num 和 times, 初始值都为1,点击 button ,分别将 num 和 times +1。
class Test1 extends Component {
state = {
num: 1,
times: 1,
}
onClick = () => {
console.log('state 1');
this.setState({
num: this.state.num + 1,
});
console.log('state 2');
this.setState({
times: this.state.times +1,
});
console.log('state 3');
}
render() {
console.log('render');
const { num, times } = this.state
return (
<div>
组件1
<button onClick={this.onClick}>更新</button>
<div>
num:{num}
</div>
<div>
times:{times}
</div>
</div>
);
}
}
控制台打印结果:
setState 异步更新,并且 2 次 setState 合并更新,render 只触发 1 次。
1.2 React 控制之外的的事件中 setState 同步更新,比如原生 js 绑定事件,异步执行的 setTimeout/setInterval, Promise.then() 等。
相同的组件,在 setTimeout 中更新状态:
onClick = () => {
setTimeout(() => {
console.log('state 1');
this.setState({
num: this.state.num + 1,
});
console.log('state 2');
this.setState({
times: this.state.times +1,
});
console.log('state 3');
}, 1000);
}
控制台打印结果:
相同的组件,在 Promise.then() 中更新状态:
onClick = () => {
new Promise((resolve,reject) => {
resolve();
}).then(() => {
console.log('state 1');
this.setState({
num: this.state.num + 1,
});
console.log('state 2');
this.setState({
times: this.state.times +1,
});
console.log('state 3');
})
}
控制台打印结果:
使用原生 addEventListener 为 button 添加点击事件
componentDidMount() {
document.getElementById('btn').addEventListener('click',() => {
console.log('state 1');
this.setState({
num: this.state.num + 1,
});
console.log('state 2');
this.setState({
times: this.state.times +1,
});
console.log('state 3');
})
}
控制台打印结果:
2. React 状态更新合并,以及如何控制异步和同步
2.1 状态更新合并
setState 可以接收两个参数,第一个参数为一个对象或者一个函数,第二个参数为一个回调函数,会在状态更新后执行,可以看做微任务。
参数为对象的 setState:
onClick = () => {
console.log('state 1');
this.setState({
num: this.state.num + 1,
})
console.log('state 2');
this.setState({
num: this.state.num + 1,
})
console.log('state 3');
this.setState({
num: this.state.num + 1,
})
console.log('state 4');
}
控制台打印结果:
点击按钮时执行三次 setState ,但实际 num 只增加 1 ,这是因为 setState 有“异步”性质,其中拿到的 this.num 相当于一个快照,并不能拿到即时更新的结果,三次 setState 中,this.state.num 都是 1,所以最后 num 是 2 。
参数为函数的 setState:
onClick = () => {
console.log('state 1');
this.setState( preState => ({
num: preState.num + 1,
}))
console.log('state 2');
this.setState( preState => ({
num: preState.num + 1,
}))
console.log('state 3');
this.setState( preState => ({
num: preState.num + 1,
}))
console.log('state 4');
}
控制台打印结果:
使用函数式参数时,可以看做“同步”,preState 可以拿到即时更新后的值,经过三次更新,num 为 4 。
结合两种,以下的更新,控制台打印的结果时什么样的:
onClick = () => {
//a
this.setState({
num: this.state.num + 1,
})
console.log('1:',this.state.num);
//b
this.setState({
num: this.state.num + 1,
})
console.log('2:',this.state.num);
setTimeout(() => {
//c
this.setState({
num: this.state.num + 1,
});
console.log('3:',this.state.num);
}, 0);
//d
this.setState(preState => ({
num: preState.num + 1,
}),() => {
console.log('4:',this.state.num);
})
//e
this.setState(preState => ({
num: preState.num + 1,
}))
console.log('5:',this.state.num);
}
控制台打印结果:
更新 c 在 setTimeout 中,即使延迟时间为 0 ,也属于宏任务;其他 4 次更新会合并,所以总共实际更新两次。d 中的 log 放在回调函数中,属于微任务,所以 5 次 log 的顺序时 1, 2, 5, 4, 3 。
第一次更新中,a, b 两次 setState 中,this.state.num 都为 1 ,所以更新后 num 为 2, d, e 两次 setState 中,preState.num 都可以拿到即时更新结果,分别为 2 ,3 所以更新后 num 为 4 。
第二次更新中,this.state.num 已经是 4 了,故更新后 num 为 5。
(实际使用中不建议两种参数形式混用。)
2.2 同步异步的控制策略
在Rect 的 setState 函数的实现中,有一个变量 isBatchingUpdate ,为 true 时表示处于批量更新模式,不进行 state 的更新操作,而是将需要更新的 component 添加到 dirtyComponents 数组中;为 false 时队列执行 batchedUpdates 更新。 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates 将 isBatchingUpdates 修改为 true ,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。
2.3 将异步更新转为同步更新
在网上搜索了一下将异步更新转变为同步更新的方法,多是使用 Promise 包装一下 setState ,调用时借助 async/await 来实现同步更新状态。
onClick = async () => {
console.log('state 1');
await this.setStateAsync({
num: this.state.num + 1,
});
console.log('state 2');
await this.setStateAsync({
times: this.state.times +1,
});
console.log('state 3');
}
setStateAsync = (state) => {
return new Promise((resolve,reject) => {
this.setState(state,() => {
resolve()
})
})
}
控制台打印结果:
这样确实使异步更新状态转变为同步更新了,但其实不需要使用 Promise 来包装 setState ,仅仅使用 async/await 就可以实现。
onClick = async () => {
console.log('state 1');
this.setState({
num: this.state.num + 1,
});
console.log('state 2');
this.setState({
num: this.state.num +1,
});
console.log('state 3');
await this.setState({
num: this.state.num +1,
});
console.log('state 4');
this.setState({
num: this.state.num +1,
});
console.log('state 5');
this.setState({
num: this.state.num +1,
});
console.log('state 6');
}
控制台打印结果:
可以看出,前三次 setState 依然是异步更新,并且合并更新了,在第三次的 setState 使用了await 之后,后两次 setState 都是同步更新。
甚至都不需要等待 setState ,任意等待一个 Promise 也有这样的效果:
onClick = async () => {
console.log('state 1');
this.setState({
num: this.state.num + 1,
});
console.log('state 2');
this.setState({
num: this.state.num +1,
});
console.log('state 3');
this.setState({
num: this.state.num +1,
});
await new Promise((resolve,reject) => {
console.log('promise start');
resolve();
})
console.log('promise end');
console.log('state 4');
this.setState({
num: this.state.num +1,
});
console.log('state 5');
this.setState({
num: this.state.num +1,
});
console.log('state 6');
}
控制台打印结果:
await 之前的三次 setState 异步更新了,await 之后的两次 setState 同步更新。
首先我们要理解 async/await 。 async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句( await 通常用来等待一个 Promise ,但是也可以等待一般的函数表达式,等待一般表达式时相当于使用 Promise.resolve() 包装其返回值)。也就是说函数体中 await 后的语句都是异步触发,此时已经脱离了 React 的调度,所以 setState 变成了同步更新。
附一道简单的 async/await 面试题,加深一下对 async/await 的理解:
async function async1(){
console.log('async1 start');
let res = await async2();
console.log(res);
console.log('async1 end');
}
async function async2(){
console.log('async2 start');
return 'async2 end'
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve,reject) => {
console.log('Promise start');
resolve();
}).then(() => {
console.log('Promise end');
})
console.log('script end');
控制台打印结果:
主线程首先打印 'script start' 后遇到 setTimeout ,函数体进入宏任务;
执行函数 async1 ,打印 'async1 start', 遇到 await ,进入函数 async2,打印 'async2 start', 返回值 'async2 end' 会被 Promsie.resolve() 包装,进入微任务;
进入 new Promsie(), 打印 'Promise start', resolve('Promise end') 进入微任务
打印 'script end' ,主线程结束。
微任务1, 函数 async2 返回, 函数 async2 等待结束,打印 'async2 end', 'async1 end'。
微任务2,Promise.then(), 打印 'Promise end'。微任务清空。
执行宏任务,打印 'setTimeout'。