react 聊聊setState异步背后的原理,react如何感知setState下的同步与异步?
壹 ❀ 引
在react中的setState是同步还是异步?react为什么要将其设计成异步?一文中,我们介绍了setState
同步异步问题,解释了何种情况下同步与异步,异步带来了什么好处,以及react
官方为何要将setState
设计成异步。
但因为文章篇幅问题,我们遗留了一个与setState
底层相关的问题,为什么在合成事件中使用setState
会批量异步合并,而原生事件中setState
又是同步呢?react
是如何感知这两者的区分从而做不同处理,带着疑问文本开始。
贰 ❀ setState背后的秘密(旧版)
既然setState
在合成与原生事件之间有所区分,那么在setState
源码实现上一定会有所表现,这里我们摘出setState
相关源码做一个简单分析。
注意,这里的源码版本为react 15
,原因是我在阅读react 16
源码过程中发现react
在更新机制上已经有了Fiber
的介入,若不了解Fiber
理解起来就十分困难了:
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
// ....
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
所以还是先了解低版本的做法,对于后续理解Fiber
也有一定帮助,先看看setState
相关实现方法,这里做了部分代码裁剪:
ReactComponent.prototype.setState = function (partialState, callback) {
// 本质上调用的是enqueueSetState这个方法
this.updater.enqueueSetState(this, partialState)
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState')
}
}
当我们调用setState
时,其实本质上调用的是this.updater.enqueueSetState
,看到enqueue
本能会想到队列,这里感觉就跟批量处理扯上关系了,OK,我们接着看enqueueSetState
的实现:
enqueueSetState: function(publicInstance, partialState) {
// 根据传递的this,获取当前组件实例
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState',
);
// 获取当前组件实例上的_pendingStateQueue
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
// 把当前要更新的状态push到数组中
queue.push(partialState);
// 再次调用enqueue更新方法
enqueueUpdate(internalInstance);
}
enqueueSetState
做的事情也很简单,大致分为四步:
- 根据传递的
this(参数publicInstance)
获取当前组件的实例internalInstance
。 - 获取组件实例
internalInstance
上的数组_pendingStateQueue
,看名字就知道是等待被处理的state
状态,而且假如不存在,这里也会帮其初始化成一个数组。 - 将我们这一次要更新的
state
状态push
到数组中。 - 调用队列更新方法
enqueueUpdate
。
接着我们来看看enqueueUpdate
相关实现:
var dirtyComponents = [];
function enqueueUpdate(component) {
ensureInjected();
// isBatchingUpdates决定了是否立刻更新this.state
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
这里,我们就看到大家有所耳闻的isBatchingUpdates(表示当前是否处理批量更新阶段)
,react
会根据此字段来决定是否立刻更新状态。假设isBatchingUpdates
为false
,直接调用batchingStrategy.batchedUpdates
做更新操作,假设为true
,则将我们当前的组件实例加入dirtyComponents
中,表示这个更新得再等一等。
那既然isBatchingUpdates
是由batchingStrategy(批量更新策略)
提供,我们接着看看它的内部实现:
var transaction = new ReactDefaultBatchingStrategyTransaction();
var ReactDefaultBatchingStrategy = {
// 全局的isBatchingUpdates,一开始默认是false
isBatchingUpdates: false,
batchedUpdates: function (callback, a, b, c, d, e) {
// 这里是用于在修改isBatchingUpdates之前存储上次的isBatchingUpdates状态
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates
// 只要调用batchedUpdates就会将isBatchingUpdates改为true
ReactDefaultBatchingStrategy.isBatchingUpdates = true
// 如果在我们改isBatchingUpdates为true之前它就已经是true了,那说明改之前就已经处于批量更新状态中了
if (alreadyBatchingUpdates) {
// 那既然已经在更新了,就直接等待更新结束
return callback(a, b, c, d, e)
} else {
// 启动事务开始进行更新
return transaction.perform(callback, null, a, b, c, d, e)
}
},
}
OK,到这里情况就有点复杂了,我们提炼下信息以及可能的疑问。
ReactDefaultBatchingStrategy
提供了全局的批量更新状态锁isBatchingUpdates
,且一开始默认是false
。- 假设我们调用
batchedUpdates
,会将isBatchingUpdates
改为true
- 根据修改
isBatchingUpdates
之前的锁的状态决定不同的处理,锁是false
直接等待更新结束,是true
那就开始走事务更新。
问题来了,虽然ReactDefaultBatchingStrategy
提供了isBatchingUpdates
但这个东西一开始就是false
啊,我们目前唯一看到的锁的修改还是在batchedUpdates
中。但很明显在上面的enqueueUpdate
中就已经先一步对于batchedUpdates
状态做判断了,那说明一定有其它地方也会修改batchedUpdates
的状态,否则同步异步的执行就完全没区别了。
我们可以尝试推理下异步与同步的差异过程,假设是异步情况,当走到enqueueUpdate
时按照我们的理解,此时isBatchingUpdates
就应该是true
,这样代码才能走到dirtyComponents.push(component)
这一步,让状态更新等一等,因此一定在更之前有什么操作将isBatchingUpdates
改为true
,这也逻辑才合理。
而同步情况参考一开始的isBatchingUpdates
默认值是false
,逻辑也确实也能走到立刻更新batchedUpdates
,但是要注意,batchedUpdates
中是会将isBatchingUpdates
改为true
的,那你在定时器中写了两个setState
,第一次因为锁的默认值是false
算你立刻更新了,但锁被改成true
了第二次同步更新怎么办?按照常理来说,一定有一个更新完成后重置锁的状态为false
的动作,不然这就说不通了。
PS:题外话说一句,不要在同一组件中同时使用同步与异步更新this.state
,由上分的分析就能感受到,这种做法极大可能造成更新的混乱与不可预期。
叁 ❀ react中的Transaction(事务)
通过上面的分析,我们已经得知用于区分是否立刻更新还是等等再更新的关键在于批量更新锁batchedUpdates
,但紧接着我们脑补了同步与异步的执行情况,推测一定有某个地方会做提前修改锁的状态,以及更新完成后重置锁状态类似的操作,那么在哪做的呢?谁来负责这一块的逻辑呢?这就得聊聊react
中的事务处理Transaction
。
class Transaction {
reinitializeTransaction() {
// 获取wapper方法,是个数组
this.transactionWrappers = this.getTransactionWrappers();
}
// 事务的启动方法
perform(method, scope, ...param) {
this.initializeAll(0);
// 这里执行的method其实就是enqueueUpdate
var ret = method.call(scope, ...param);
this.closeAll(0);
return ret;
}
// 执行所有wapper中的init
initializeAll(startIndex) {
var transactionWrappers = this.transactionWrappers;
for (var i = startIndex; i < transactionWrappers.length; i++) {
var wrapper = transactionWrappers[i];
wrapper.initialize.call(this);
}
}
// 执行所有wapper中的close
closeAll(startIndex) {
var transactionWrappers = this.transactionWrappers;
for (var i = startIndex; i < transactionWrappers.length; i++) {
var wrapper = transactionWrappers[i];
wrapper.close.call(this);
}
}
}
class ReactDefaultBatchingStrategyTransaction extends Transaction {
constructor() {
this.reinitializeTransaction()
}
// 返回wapper方法,是个数组
getTransactionWrappers() {
return [
// FLUSH_BATCHED_UPDATES
{
initialize: () => { },
// state更新完后,diff对比以及组件后续更新
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
},
// RESET_BATCHED_UPDATES
{
initialize: () => { },
close: () => {
// 重置锁
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
}
]
}
}
react
的事务解释起来有点抽象,但我们可以先站在宏观的角度去理解,我们可以将Transaction
理解成一个封闭的方法加工工厂,每当一个方法运输进去,都会通过wapper
对方法进行加工,并为方法组装上initialize
与close
方法。
当调用transaction.perform
启动事务处理时,你会发现在perform
中的处理分为三步,第一执行所有wapper
中的init
;第二才是真正执行我们传递的callback
(本质就是enqueueUpdate
);第三步在callback
跑完再执行所有wapper
中的close
。
而wapper
其实也分为FLUSH_BATCHED_UPDATES
与RESET_BATCHED_UPDATES
两种类型,不同类型中的init
我们先不管,但FLUSH_BATCHED_UPDATES
中的close
会在callback
执行完成后帮助我们更新最新的state
与props
。
当我们看向RESET_BATCHED_UPDATES
的close
时,我们发现了一个熟悉的操作ReactDefaultBatchingStrategy.isBatchingUpdates = false
,这里是我们第二次发现修改锁的状态。还记得前面我们对于原生定时器中多次执行setState
的问题吗?第一次setState
会将isBatchingUpdates
改为true
,但在执行完完成后RESET_BATCHED_UPDATES
中的close
会帮我们立刻重置锁的状态,这也就保证了定时器中第二个setState
运行时,锁的状态又默认成了false
,于是再次同步更新。
肆 ❀ 为什么钩子合成事件是异步?
事务介绍了一大堆,我们顺利解释了同步更新情况下setState
是如何重置锁状态的,那么钩子函数执行setState
得保证锁一开始就是true
才行啊,这又是怎么回事呢?看下面这段代码:
// 摘自上方的batchedUpdates方法,知道里面有将锁改为true的操作就行
batchedUpdates: function (callback, a, b, c, d, e) {
// ...
ReactDefaultBatchingStrategy.isBatchingUpdates = true
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e)
} else {
return transaction.perform(callback, null, a, b, c, d, e)
}
}
_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
var componentInstance = instantiateReactComponent(nextElement);
// 调用batchedUpdates方法
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context
);
}
_renderNewRootComponent
其实看名字就知道是在组件渲染时执行的方法,也就是在组件初次渲染,这里就已经执行过一次batchedUpdates
方法了,而batchedUpdates
内部有将锁改为true
的操作,这也就是为啥钩子函数中setState
异步的问题。
同理,合成事件中的setState
也是异步,那说明也应该有初始锁状态为true
的行为,事实上确实如此,看下面代码:
dispatchEvent: function (topLevelType, nativeEvent) {
try {
// 调用了batchedUpdates修改锁状态
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
那么到这里,我们解释了钩子函数,合成事件以及原生事件中setState
执行差异背后的原理。
伍 ❀ 总
其实本文一开始我是打算通过setState
的差异性引出合成事件,从而介绍合成事件与原生事件的区别。但在整理setState
的过程中,发现信息量惊人....而且更为离谱的是,react
从16.8
引入fiber
开始,setState
原理其实已经不再是上述那样了。但处于篇幅问题以及知识量,此篇仅介绍react 15
同异步原理差异,那么下一篇正式介绍react
中的合成事件,本文结束。