这个问题相信是大部分的人刚开始学习React
的时候,第一个碰到的问题,对我来讲,学习的教程里就只是告诉我直接使用setState
是异步的,可是在一些好比setTimeout
这样的异步方法里,它是同步的。数组
我那时候就很疑惑,虽然想要一探究竟,可是为了尽快上手React
,仍是以应用优先,留到以后的源码学习时,再来深刻了解一下。缓存
本文的内容就是从源码层面来分析setState
究竟是同步仍是异步的。由于如今作的项目其实都是使用hooks
在维护数据状态,对于class
使用特别少,因此我并不会太过深究底层的渲染原理,重点只是在于为何setState
有的时候表现是同步,有的时候表现是异步。性能优化
export default class App extends React.Component{
state = {
num: 0
}
add = () => {
console.log('add前', this.state.num)
this.setState({
num: this.state.num + 1
});
console.log('add后', this.state.num)
}
add3 = () => {
console.log('add3前', this.state.num)
this.setState({
num: this.state.num + 1
});
this.setState({
num: this.state.num + 1
});
this.setState({
num: this.state.num + 1
});
console.log('add3后', this.state.num)
}
reduce = () => {
setTimeout(() => {
console.log('reduce前', this.state.num)
this.setState({
num: this.state.num - 1
});
console.log('reduce后', this.state.num)
},0);
}
render () {
return <div> <button onClick={this.add}>点击加1</button> <button onClick={this.add3}>点击加3次</button> <button onClick={this.reduce}>点我减1</button> </div>
}
}
复制代码
按顺序依次点击这三个按钮,咱们来看下控制台打印出来的内容。markdown
这个结果,对于有点经验React
开发者来讲很简单。app
按照这个例子来看,setState
是一个异步的方法,执行完成以后,数据不会立刻修改,会等到后续某个时刻才进行变化。屡次调用setState
,只会执行最新的那个事件。在异步的方法中,它会有同步的特性。异步
咱们先不着急下结论,咱们深刻setState
的流程中去找结论。ide
出于性能优化的须要,一次setState
是不会触发一个完整的更新流程的,在一个同步的代码运行中,每次执行一个setState
,React
会把它塞进一个队列里,等时机成熟,再把“攒起来”的state
结果作合并,最后只针对最新的state
值走一次更新流程。这个过程,叫做批量更新。函数
这样子,就算咱们代码写的再烂,好比写了一个循环100次的方法,每次都会调用一个setState
,也不会致使频繁的re-render
形成页面的卡顿。性能
这个原理,解释了上面第一个按钮以及第二个按钮的现象。学习
这里的问题就一个,为何setTimeout
能够将setState
的执行顺序从异步变为同步?
咱们来看看setState
的源码
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
复制代码
不考虑callback
回调,这里其实就是触发了一个enqueueSetState
方法。
enqueueSetState: function (publicInstance, partialState) {
// 根据 this 拿到对应的组件实例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 这个 queue 对应的就是一个组件实例的 state 数组
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate 用来处理当前的组件实例
enqueueUpdate(internalInstance);
}
复制代码
这个方法就是刚才说的,把state
的修改放进队列中。而后使用enqueueUpdate
来处理将要更新的组件实例。再来看看enqueueUpdate
方法。
function enqueueUpdate(component) {
ensureInjected();
// 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量建立/更新组件的阶段
if (!batchingStrategy.isBatchingUpdates) {
// 若当前没有处于批量建立/更新组件的阶段,则当即更新组件
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 不然,先把组件塞入 dirtyComponents 队列里,让它“再等等”
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
复制代码
这里重点关注一个对象,batchingStrategy
(React
内部专门用于管控批量更新的对象),它的属性isBatchingUpdates
直接决定了当下是否要走更新流程,仍是应该等等。
每当React
调用batchedUpdate
去执行更新动做时,会先把这个锁给“锁上”(置为true
),代表“如今正处于批量更新过程当中”。当锁被“锁上”的时候,任何须要更新的组件都只能暂时进入dirtyComponents
里排队等候下一次的批量更新。
var ReactDefaultBatchingStrategy = {
// 全局惟一的锁标识
isBatchingUpdates: false,
// 发起更新动做的方法
batchedUpdates: function(callback, a, b, c, d, e) {
// 缓存锁变量
var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
// 把锁“锁上”
ReactDefaultBatchingStrategy. isBatchingUpdates = true
if (alreadyBatchingStrategy) {
callback(a, b, c, d, e)
} else {
// 启动事务,将 callback 放进事务里执行
transaction.perform(callback, null, a, b, c, d, e)
}
}
}
复制代码
这里,咱们还须要了解React
中的Transaction
(事务) 机制。
Transaction
在React
源码中表现为一个核心类,Transaction
能够建立一个黑盒,该黑盒可以封装任何的方法。所以,那些须要在函数运行前、后运行的方法能够经过此方法封装(即便函数运行中有异常抛出,这些固定的方法仍可运行),实例化Transaction
时只需提供相关的方法便可。
* <pre> * wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ * </pre>
复制代码
Transaction
就像是一个“壳子”,它首先会将目标函数用wrapper
(一组initialize
及close
方法称为一个wrapper
) 封装起来,同时须要使用Transaction
类暴露的perform
方法去执行它。如上面的注释所示,在anyMethod
执行以前,perform
会先执行全部 wrapper
的initialize
方法,执行完后,再执行全部wrapper
的close
方法。这就是React
中的事务机制。
结合咱们刚才的点击事件,事件实际上是做为一个callback
回调函数在事务中调用的,调用以前,批量更新策略事务会把isBatchingUpdates
置为true
,而后执行callback
方法,执行完毕以后,把isBatchingUpdates
置为false
,而后再循环全部的dirtyComponents
调用updateComponent
更新组件。
因此刚才的点击事件,其实能够这样理解
add = () => {
// 进来先锁上
isBatchingUpdates = true
console.log('add前', this.state.num)
this.setState({
num: this.state.num + 1
});
console.log('add后', this.state.num)
// 执行完函数再放开
isBatchingUpdates = false
}
复制代码
这种状况下,setState
是异步的。
咱们再来看看setTimeout
的状况
reduce = () => {
// 进来先锁上
isBatchingUpdates = true
setTimeout(() => {
console.log('reduce前的', this.state.num)
this.setState({
num: this.state.num - 1
});
console.log('reduce后的', this.state.num)
},0);
// 执行完函数再放开
isBatchingUpdates = false
}
复制代码
由于setTimeout
是在以后的宏任务中执行的,因此这时候运行的setState
,isBatchingUpdates
已经被置为false
了,它会当即执行更新,因此具有了同步的特性。setState 并非具有同步这种特性,只是在某些特殊的执行顺序下,脱离了异步的控制
setState
并非单纯同步/异步的,它的表现会因调用场景的不一样而不一样:在React
钩子函数及合成事件中,它表现为异步;而在 setTimeout
、setInterval
等函数中,包括在DOM
原生事件中,它都表现为同步。这种差别,本质上是React
事务机制和批量更新机制的工做方式来决定的。