前言
你觉得setState
是同步的还是异步的?
先举一个例子,在页面上有三个按钮:+1, + 3和-1,依次点击三个按钮,猜测输出结果:
import * as React from 'react';
import { Component } from 'react';
import './App.css'
import { AppWrapper } from './style'
class App extends Component{
state = {
count: 0
}
increment = () => {
console.log('增加1之前', this.state);
this.setState({
count: this.state.count + 1
})
console.log('增加1之后', this.state);
}
incrementThree = () => {
console.log('增加3之前', this.state);
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
console.log('增加3之后', this.state);
}
reduce = () => {
setTimeout(() => {
console.log('减少1之前', this.state);
this.setState({
count: this.state.count - 1
})
console.log('减少1之后', this.state);
})
}
render(){
return (
<>
<button onClick={this.increment}>点击增加1</button>
<button onClick={this.incrementThree}>点击增加3</button>
<button onClick={this.reduce}>点击减少1</button>
</>
)
}
}
export default App
设计动机
从生命周期角度来看,当组件调用setState
更新数据时,会依次触发下面的钩子函数:
当我们多次调用setState
时,就会重复调用这个更新流程,即re-render
,将会有很大的性能开销,所以为了避免的re-render
,setState
就设计成了异步。在React
真正运行时,每来一个setState
,就把它塞进一个队列里“攒起来”,等时机成熟,再将缓存的state
进行合并,最后只对最新的state
值走一次更新流程。这个过程叫做“批量更新”。
从源码解读setState的工作流
先来看一张流程图:
setState
先在react
中找到setState
:
ReactComponent.prototype.setState = function (partialState, callback) {
// 将setState事务放入enqueueSetState队列中
this.updater.enqueueSetState(this, partialState);
// 将setState的回调函数放入enqueueCallback放入队列中
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
partialState
有部分state
的含义,即只是影响state
,不会伤及无辜。
enqueueSetState
enqueueSetState: function (publicInstance, partialState) {
// 先获取ReactComponent组件对象
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
// queue即对应该组件实例的 state 数组
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// 将要更新的ReactComponent放入数组中
enqueueUpdate(internalInstance);
}
enqueueUpdate
function enqueueUpdate(component) {
ensureInjected();
// 用isBatchingUpdates来判断当前是否处于批量创建/更新组件的阶段
// 如果不是正处于创建或更新组件阶段,则立即更新组件
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果正在创建或更新组件,则暂且先不处理,只是将组件放在dirtyComponents数组中,稍后再处理
dirtyComponents.push(component);
}
isBatchingUpdates=true
表明当前处于更新事务状态中,将component
存入dirtyComponent
,否则调用batchedUpdates
,发起一个transaction.perform
。
isBatchingUpdates(批量更新策略)
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
// 发起更新动作
batchedUpdates: function (callback, a, b, c, d, e) {
// 缓存锁变量
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
// 批处理最开始时,将isBatchingUpdates设为true,表明正在更新(上锁)
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
// 以事务的方式处理updates,把callback放进transaction中,后面详细分析transaction
transaction.perform(callback, null, a, b, c, d, e);
}
}
}
事务transaction
简单地说,一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。
Transaction事务说白了就是在不改变原有方法的基础上,在执行方法的前后进行额外的操作。具体来说,就是一个方法会被wrapper
包裹,且方法需要通过perform
来调用,且在被包裹方法的前后分别执行initialize
和close
。
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
// 事务批更新处理结束时,将isBatchingUpdates设为了false
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
RESET_BATCHED_UPDATES
中的code
阶段会将isBatchingUpdates
设置为false
,而FLUSH_BATCHED_UPDATES
中的close
会执行flushBatchedUpdates
,里面包含了Vitual DOM
到真实DOM的映射等其他操作,即更新DOM。
flushBatchedUpdates
这个方法会遍历所有的dirtyComponents
,更新组件主要调用了performUpdateIfNecessary
,又卷入到了事务中。
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
// 进行一个排序操作,这里因为通常情况下,父组件更新后,子组件也会随之更新
// 所以这里进先进行排序,使得子组件在父组件之前被更新
dirtyComponents.sort(mountOrderComparator);
// 代表批量更新的次数, 保证每个组件只更新一次
updateBatchNumber++;
// 遍历 dirtyComponents
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
...
// 执行更新
ReactReconciler.performUpdateIfNecessary(
component,
transaction.reconcileTransaction,
updateBatchNumber,
);
...
// 存储 callback以便后续按顺序调用
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(
callbacks[j],
component.getPublicInstance(),
);
}
}
}
}
performUpdateIfNecessary
ReactReconciler
会调用组件实例的performUpdateIfNecessary
. 如果接收了props, 就会调用此组件的receiveComponent
, 再在里面调用updateComponent
更新组件; 如果没有接受props, 但是有新的要更新的状态(_pendingStateQueue
不为空)就会直接调用updateComponent
来更新:
performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
// 在组件更新时,this._prndingStateQueue = true时,就会一直调用updateComponent。
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
} else {
this._updateBatchNumber = null;
}
}
updateComponent
该方法中,主要使用_processPendingState
对state
进行了操作
_processPendingState: function (props, context) {
var inst = this._instance; // _instance保存了Constructor的实例,即通过ReactClass创建的组件的实例
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null;
if (!queue) {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = _assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;
}
- 如果更新队列为
null
,则返回原来的state
; - 如果更新队列有一个值,则返回更新值;
- 如果更新队列有多个值,则
for
循环将它们合并。
由此可见,在一个生命周期中所有的state
变化都会被合并并统一处理。
另外updateComponent
中还会在合并state
之后、新state
映射到DOM
之前,先调用shouldComponentUpdate()
来判断是否更新组件。
setState流程图
在调用setState
时,会先判断是否处于批量创建/更新组件的阶段,如果不是,再进入设置isBatchingUpdates
为true
,并执行事务机制进行更新;如果刚好处于批量创建/更新组件的过程中,则存入dirtyComoponent
,然后遍历dirtyComponent
循环更新组件,有卷入到了事务处理中。等事务处理完成isBatchingUpdates
为false
,然后再shouldComponentUpdate
前合并state
,通过shouldComponentUpdate
判断组件是否要更新。
常见的错误使用方式
使用this.state.xxx = xxx更新
从上面可知setState
通过一个队列实现state
更新。当执行setState
时,会将需要更新的state
合并后放入状态队列,而不会立刻更新this.state
,队列可以高效地批量更新state
。如果直接修改this.state
,那么该state
将不会被放入状态队列中,即会被忽略。
在shouldComponentUpdate或componentWillUpdate更新state
从上面源码的分析我们可以总结出setState
的流程,当我们更新组件时,组件中由于使用setState
也会进行state
的合并,当state
合并成功之后,则对组件进行更新。组件更新调用了updateComponent
方法,该方法会合并state
,并跟shouluComponetUpdate
来判断是否更新直接。
如果我们在shouldComponentUpdata
或者componentWillUpdate
方法中调用setState
,此时this._pendingStateQueue != null
,则perfromUpdateIfNessary
就会调用updateComponent
进行组件更新,但是updateComponent
方法又会调用shouldComponentUpdate
和componentWillUpdate
方法,因此造成循环引用,使得浏览器内存占满而奔溃。
这也可以理解为什么React 16中为什么要废除这两个钩子了。
输出结果解释
最前面那个例子我们可以理解成:
- 当我们点击按钮时,由于此时已经存在在一个大事务中(不必深究),此时
isBatchingUpdate=true
,所以increment
和incrementThree
两次setState
都没有立即生效,而是放置在了dirtyComponents
中,新的state
等待合并和更新到组件中,所以this.state.couut = 0
。 - 而
setTimoutout
的逻辑时异步执行的,此时内部的setState
执行时isBatchingUpdate=false
,所以state
立即更新。
总结
从上面可知:
setState
并不能保证同步更新,但是也不能说它的更新方式是异步的。在React钩子函数及合成事件中,它表现为异步,但在setTimeout、setInterval
等函数中,它表现为同步;setState
实现异步的机制是为了防止用户多次setState
导致频繁的re-render
;- 由于组件更新会调用
updateComponent
,在合并state
之后会调用shouldComponentUpdate
,所以我们不能在shouldComponentUpdate
或者componentWillUpdate
中使用setState
,不然会导致死循环; - 使用
this.state.xxx = xxx
更新数据并不会加入到state
的更新队列中,即无法更新数据。
参考
如有错误,欢迎指出,感谢~