探究React-setState原理
1、批量更新
我们先来了解一下批量更新。
先从一段代码例子说起:
import React from "react";
export default class App extends React.Component {
state = {
count: 0,
};
firstIncrement = () => {
console.log("增加1前", this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log("增加1后", this.state.count);
};
secondIncrement = () => {
console.log("增加2前", this.state.count);
this.setState({
count: this.state.count + 1,
});
this.setState({
count: this.state.count + 1,
});
console.log("增加2后", this.state.count);
};
reduce = () => {
setTimeout(() => {
console.log("减少1前", this.state.count);
this.setState({
count: this.state.count - 1,
});
console.log("减少1后", this.state.count);
}, 0);
};
render() {
return (
<div>
<button onClick={this.firstIncrement}>按钮1-增加1</button>
<button onClick={this.secondIncrement}>按钮2-增加2</button>
<button onClick={this.reduce}>减少1</button>
</div>
);
}
}
依次点击操作结果:
从上图结果可以看出,我们直接操作setState打印结果时,是异步的,拿不到最新的值。进行多次setState时结果会被合并只执行一次。放在setTime中会变成同步可以立即获取到值。
站在生命周期角度看setState更新:
从图上我们可以看出,一个完整的更新流程,涉及了包括 re-render(重新渲染) 在内的多个步骤。
re-render 本身涉及对 DOM 的操作,它会带来较大的性能开销。
如果一次 setState 就触发一个完整的更新流程,那么每一次 setState 的调用都会触发一次 re-render,我们的视图很可能没刷新几次就卡死了。
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->render->componentDidUpdate
});
综上所述,React在设计setState时异步的一个重要的动机就是避免频繁的re-render。
所以React在实现setState异步采用批量更新操作,使用一个队列把他存起来,每次进来一个setState,就进行入队操作,等时机成熟,把state的值做合并,最后只针对最新的 state 值走一次更新流程。
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务,count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队, [count+1的任务,count+1的任务, count+1的任务]
});
↓
合并 state,[count+1的任务]
↓
执行 count+1的任务
注意:只要我们的同步代码在执行,入队这个操作就不会停止,所以最终只有一次+1生效。
test = () => {
console.log('循环100次 setState前的count', this.state.count) // 0
for(let i = 0; i < 100 ; i++) {
this.setState({
count: this.state.count + 1
})
}
console.log('循环100次 setState后的count', this.state.count) // 0
}
2、同步现象本质
setState 的同步现象:
reduce = () => {
setTimeout(() => {
console.log('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1
});
console.log('reduce setState后的count', this.state.count)
},0);
}
当我们加入了setTimeout函数,才有了同步现象。事实上并不是 setTimeout 改变了 setState,而是 setTimeout 中的 setState脱离了React事件系统,所以就变成了同步,为什么会出现这种现象呢?
我们首先来了解一下锁的概念:
batchingStrategy 是 React 内部专门用于管控批量更新的对象,你可以把他理解为锁管理器。
锁是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入队列中排队等候下一次的批量更新,而不能随意“插队”。
isBatchingUpdates 这个变量,在 React 的生命周期函数以及合成事件执行前,已经被 React 悄悄修改为了 true,这时我们所做的 setState 操作自然不会立即生效。当函数执行完毕后,事务的 close 方法会再把 isBatchingUpdates 改为 false。
在 isBatchingUpdates 的约束下,setState 只能是异步的。示例代码:
increment = () => {
// 进来先锁上
isBatchingUpdates = true
console.log('increment setState前的count', this.state.count)
this.setState({
count: this.state.count + 1
});
console.log('increment setState后的count', this.state.count)
// 执行完函数再放开
isBatchingUpdates = false
}
加入setTimeout后 ,事情就会发生一点点变化: isBatchingUpdates,对 setTimeout 内部的执行逻辑完全没有约束力。
reduce = () => {
// 进来先锁上
isBatchingUpdates = true
setTimeout(() => {
console.log('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1
});
console.log('reduce setState后的count', this.state.count)
},0);
// 执行完函数再放开
isBatchingUpdates = false
}
isBatchingUpdates 是在同步代码中变化的,而 setTimeout 的逻辑是异步执行的。当 this.setState 调用真正发生的时候,isBatchingUpdates 早已经被重置为了 false,这就使得当前场景下的 setState 具备了立刻发起同步更新的能力。setState 并不是具备同步这种特性,只是在特定的情境下,它会从 React 的异步管控中“逃脱”掉。
综上所述,在 React 事件执行之前通过 isBatchingEventUpdates=true 打开开关,开启事件批量更新,当该事件结束,再通过 isBatchingEventUpdates = false; 关闭开关。所以批量更新规则被打破。
3、手动开启批量更新
那么,如何在如上异步环境下,继续开启批量更新模式呢?React-Dom 中提供了批量更新方法 unstable_batchedUpdates,可以去手动批量更新,可以将上述 setTimeout 里面的内容做如下修改:
import ReactDOM from 'react-dom'
const { unstable_batchedUpdates } = ReactDOM
setTimeout(()=>{
unstable_batchedUpdates(()=>{
this.setState({ number:this.state.count + 1 })
console.log(this.state.count)
this.setState({ number:this.state.count + 1})
console.log(this.state.count)
this.setState({ number:this.state.numbercount+ 1 })
console.log(this.state.count)
})
})
4、总结
- setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为同步。这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的。
- setState 并非真异步,只是看上去像异步。在源码中,通过 isBatchingUpdates 来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。那么什么情况下 isBatchingUpdates 会为 true 呢?在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。但在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。一般认为,做异步设计是为了性能优化、减少渲染次数,React 团队还补充了两点。保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是。启用并发更新,完成异步渲染。
转载自:https://juejin.cn/post/7056582717537779726