https://zhuanlan.zhihu.com/p/39512941
setState 的同步和异步
1.setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。注意看下方的代码
2.setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
函数组件中
function countAdd(){
// console.log(count); //0
// setCount(count+ 1);
// console.log(count); // 0; '异步'
setTimeout(() => {
console.log(count); //0
setCount(count+ 1);
console.log(count); // 0; '异步'
}, 1);
}
return <div style={{fontSize: '10rem'}} onClick={countAdd}>
count Add {count}
</div>
类组件中
class Test2 extends Component{
constructor() {
super();
this.state = {
count: 0,
}
}
countAdd = () => {
// console.log(this.state.count); // 0
// this.setState({
// count: this.state.count + 1,
// });
// console.log(this.state.count); // 0 '异步'
setTimeout(() => {
console.log(this.state.count); // 0
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count); // 1 '同步'
}, 1);
};
componentDidMount() {
document.getElementById('testDom').addEventListener('click', ()=>{
console.log(this.state.count); // 0
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count); // 1 '同步'
}, false);
}
render() {
const {count} = this.state;
return <div style={{fontSize: '10rem'}} id={'testDom'}>
count Add {count}
</div>;
}
}
3.setState 的批量更新优化也是建立在(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。(V18之前的半自动批处理)
React18新特性:Automatic batching 自动批处理
React18新特性:Automatic batching 魔术师卡颂
- 什么是批处理(batchedUpdates):React会尝试将同一上下文中触发的更新合并为一个更新
批处理的好处
- 合并不必要的更新,减少更新流程调用次数
- 状态按顺序保存下来,更新时不会出现「竞争问题」
- 最终触发的更新是异步流程,减少浏览器掉帧可能性
批处理的几种类型
- v18的「批处理」是自动的
- v18之前的 React 使用半自动的「批处理」.
- React同时提供了一个API 手动 「批处理」unstable_batchedupdates .
半自动批处理
- 在v18之前,只有事件回调、生命周期回调中的更新会批处理,比如上例中的onClick。而在promise、setTimeout等异步回调中不会批处理。
batchedUpdates(onClick, e);
batchedUpdates方法是同步调用的。
如果异步fn的话 在真正执行setState时 batchedUpdates 早已执行完,
executionContext中已经不包含BatchedContext。 此时触发的更新不会走批处理逻辑。
「只对同步流程中的this.setState进行批处理」,只能说是「半自动」。
手动批处理
onClick() {
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
this.setState({a: 3});
this.setState({a: 4});
})
})
}
那么两次this.setState调用时上下文中全局变量executionContext中会包含BatchedContext。
v18的自动批处理如何实现的?
- 增加调度的流程
- 不以全局变量executionContext为批处理依据,而是以更新的「优先级」为依据
什么是优先级?
调用this.setState后源码内部会依次执行:
- 根据当前环境选择一个「优先级」lane
- 创造一个代表本次更新的update对象,赋予他步骤1的优先级
- 将update挂载在当前组件对应fiber(虚拟DOM)上
- 进入调度流程
每次调用this.setState会产生update对象,根据调用的场景他会拥有不同的lane(优先级)
调度流程
在组件对应fiber挂载update后,就会进入「调度流程」。
试想,一个大型应用,在某一时刻,应用的不同组件都触发了更新。
那么在不同组件对应的fiber中会存在不同优先级的update。选出这些update中优先级最高的那个,以该优先级进入更新流程。
整个过程
- 获取当前所有优先级中最高的优先级
- 将步骤1的优先级作为本次调度的优先级
- 看是否已经存在一个调度
- 如果已经存在调度,且和当前要调度的优先级一致,则return
- 不一致的话就进入调度流程
自动批处理流程
onClick() {
this.setState({a: 3});
this.setState({a: 4});
this.setState({a: 5});
this.setState({a: 6});
}
只有第一次调用会执行调度,后面几次执行由于优先级和第一次一致会return。
当一定时间过后,第一次调度的回调函数performConcurrentWorkOnRoot会执行,进入更新流程。
由于每次执行this.setState都会创建update并挂载在fiber上。
所以即使只执行一次更新流程,还是能将状态更新到最新。
这就是以「优先级」为依据的「自动批处理」逻辑。