环境
React 16.9.0
React-Dom 16.9.0
前言
从下面代码的运行结果可以得出如下结论:
- setTimeout和原生事件中,可以立即拿到更新结果。也就是同步
- 在合成事件和生命周期中,不能立即拿到更新结果。也就是所谓的“异步”
- 在合成事件和生命周期中,如果对同一个值进行多次
setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行
class App extends React.Component {
constructor () {
super();
this.state = {
counter: 0
};
}
componentDidMount() {
// 生命周期中调用
console.log("componentDidMount before: " + this.state.counter);
this.setState({ counter: this.state.counter + 1 });
// 此处不能实时更新
console.log("componentDidMount after: " + this.state.counter);
setTimeout(() => {
// setTimeout中调用
console.log("setTimeout before: " + this.state.counter);
this.setState({ counter: this.state.counter + 1 });
console.log("setTimeout after: " + this.state.counter);
}, 0);
document.getElementById("btn-2").addEventListener("click", this.btn2Click);
}
spanClick = () => {
const { counter } = this.state;
console.log("spanClick before: " + this.state.counter);
this.setState({
counter: counter + 1
})
this.setState({
counter: counter + 2
})
// 此处不能实时更新
console.log("spanClick after: " + this.state.counter);
}
btn2Click = () => {
const { counter } = this.state;
console.log("addEventListener btn2Click before: " + this.state.counter);
this.setState({
counter: counter + 1
})
// 此处可以实时更新
console.log("addEventListener btn2Click after: " + this.state.counter);
}
render () {
return (
<div className="App">
<span className="btn" onClick={(event) => this.spanClick(event)}>
点击
</span>
<span className="btn-2" id="btn-2">
点击2
</span>
</div>
)};
}
// 打印结果。before与after相同即为“异步”,否则为同步
// componentDidMount before: 0
// componentDidMount after: 0
// setTimeout before: 1
// setTimeout after: 2
// spanClick before: 2
// spanClick after: 2
// addEventListener btn2Click before: 4
// addEventListener btn2Click after: 5
看过很多篇这样标题的文章,看到过很多描述不同但意思如上的结论,但是仍然有一些疑问:
- 为什么setTimeout和原生事件会同步更新?
- “异步”情况究竟是在什么时候对state进行的更新?
为什么setTimeout和原生事件会同步更新
究竟是同步更新还是异步更新,取决于代码的执行环境。React定义了一个内部变量executionContext
(默认为NoContext
),在进行合成事件和生命周期处理的时候,会首先给该变量赋值为DiscreteEventContext
(合成事件)或 在componentDidMount中
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
来标记其现在所处的执行环境。
而在setTimeout以及原生事件中,是脱离了这些执行环境的,executionContext
就是默认值NoContext
;。下图为原生事件执行时的截图
在scheduleWork
处理逻辑的时候,如果执行环境不为NoContext
,则仅仅是将更新放在一个队列里面,不进行实际的应用(即调用flushSyncCallbackQueue
)。
结论
是否是同步更新的,取决于其执行环境。因为setTimeout和原生事件脱离了原本的执行环境,所以其state的更新为同步更新。
“异步”场景下,什么时候对state进行的更新
那在合成事件和生命周期中,又是什么时候调用的flushSyncCallbackQueue
,下面代码中的输出又是什么?
class App extends React.Component {
constructor () {
super();
this.state = {
counter: 0
};
}
appClick = () => {
console.log('--------appClick---------');
const { counter } = this.state;
console.log(this.state.counter);
this.setState({
counter: counter + 1
})
console.log(this.state.counter); // 会输出2还是0?
}
spanClick = () => {
const { counter } = this.state;
this.setState({
counter: counter + 1
})
console.log(this.state.counter);
this.setState({
counter: counter + 2
})
console.log(this.state.counter);
}
render () {
return (
<div className="App" onClick={(event) => this.appClick(event)}>
<span className="btn" onClick={(event) => this.spanClick(event)}>
点击
</span>
</div>
)};
}
如果对React原生事件有了解,以click
事件为例,会知道React在处理一次点击事件时,将所有的回调放在了一个队列里面。
参考 https://segmentfault.com/a/11...
就是在该队列执行完毕之后调用的flushSyncCallbackQueue
。因此上面的示例代码中,所有的打印都为0
;
其执行过程可以用如下代码简单表示:
var a = 1;
var updateQueue = [];
function setState (payload) {
updateQueue.push(payload);
}
function func () {
updateQueue = [];
try {
// 将本次需要调用的放在一起
setState({a: 1});
// 输出为1
console.log(window.a);
setState({a: 3});
// 输出为1
console.log(window.a);
} finally {
// 模拟最后一次性提交更新
window.a = updateQueue.reduce((accumulator, currentValue) => {
return currentValue.a || accumulator;
}, window.a)
}
}
// 运行
func()
// 输出为3
console.log(window.a);
结论
对生命周期或者合成事件包裹了一层try { // 执行,更新放队列 } finally { // 更新state }
,最后在finally
中进行的state更新。所谓的异步并不是真正的异步,而是先将更新放在了队列里面,当代码执行完后,再(在 finally 中)一次性处理这些更新
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。