旧版本的setState
对于React15来说,我们用一个例子来看看
import React from 'react';
class App extends React.Component {
state = { val: 0 }
componentDidMount() {
// 第一次调用
this.setState({ val: this.state.val + 1 });
console.log('first setState', this.state);
// 第二次调用
this.setState({ val: this.state.val + 1 });
console.log('second setState', this.state);
// 第三次调用
this.setState({ val: this.state.val + 1 }, () => {
console.log('in callback', this.state)
});
}
render() {
return <div> val: { this.state.val } </div>
}
}
export default App;
对于本例子,最后state的值只会是1,而且输出的时候应该是以下这样的:
我们只有在对应的回调函数中才可以获取到最新的值,那么我们可能会误认为setState是异步操作
但是实际上setState 是一次同步操作,只是每次操作之后并没有立即执行,而是将 setState 进行了缓存,mount 流程结束或事件操作结束,才会拿出所有的 state 进行一次计算。如果 setState 脱离了 React
的生命周期
或者 React 提供的事件流
,setState 之后就能立即拿到结果。
在进一步学习之前,我们需要学习一下React相关的事务
从源码出发
我们将会从React15开始探查
我们React中组件的setState方法是挂载在原型上面的
// 对外暴露的 React.Component
function ReactComponent() {
this.updater = ReactUpdateQueue;
}
// setState 方法挂载到原型链上
ReactComponent.prototype.setState = function (partialState, callback) {
// 调用 setState 后,会调用内部的 updater.enqueueSetState
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
var ReactUpdateQueue = {
enqueueSetState(component, partialState) {
// 在组件的 _pendingStateQueue 上暂存新的 state
if (!component._pendingStateQueue) {
component._pendingStateQueue = [];
}
var queue = component._pendingStateQueue;
queue.push(partialState);
enqueueUpdate(component);
},
enqueueCallback: function (component, callback, callerName) {
// 在组件的 _pendingCallbacks 上暂存 callback
if (component._pendingCallbacks) {
component._pendingCallbacks.push(callback);
} else {
component._pendingCallbacks = [callback];
}
enqueueUpdate(component);
}
}
我们可以发现,在调用setState后,会调用updater.enqueSetState函数
根据updater.enqueSetState函数,我们可以发现在调用我们这个updater.enqueSetState函数时并没有直接更新state,而是将传入的参数放在组件内部的_pendingStateQueue队列中(代码21行),之后再去调用enqueueUpdate(component);(代码22行)来执行更新的过程。
那么我们需要对于enqueueUpdate(component)进行进一步探索
var dirtyComponents = [];
function enqueueUpdate(component) {
if (!batchingStrategy.isBatchingUpdates) {
// 开始进行批量更新
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果在更新流程,则将组件放入脏组件队列,表示组件待更新
dirtyComponents.push(component);
}
我们可以看到enqueueUpdate首先会通过batchingStrategy.isBatchingUpdates去判断当前是否在更新流程中,不在的话那么会调用batchingStrategy.batchedUpdates(enqueueUpdate, component)进行更新;在流程中,会push到dirtyComponents中进行缓存。
那么我们需要进一步对于batchingStrategy进行进一步探索了。
batchingStrategy是React进行批处理的一种策略,该策略的实现基于Transaction
我们需要对于 Transaction有一点了解,因此我们需要对于 Transaction的源码进行相关查看
class Transaction {
reinitializeTransaction() {
this.transactionWrappers = this.getTransactionWrappers();
}
perform(method, scope, ...param) {
this.initializeAll(0);
var ret = method.call(scope, ...param);
this.closeAll(0);
return ret;
}
initializeAll(startIndex) {
var transactionWrappers = this.transactionWrappers;
for (var i = startIndex; i < transactionWrappers.length; i++) {
var wrapper = transactionWrappers[i];
wrapper.initialize.call(this);
}
}
closeAll(startIndex) {
var transactionWrappers = this.transactionWrappers;
for (var i = startIndex; i < transactionWrappers.length; i++) {
var wrapper = transactionWrappers[i];
wrapper.close.call(this);
}
}
}
Transaction
通过 perform 方法启动,然后通过扩展的 getTransactionWrappers
获取一个数组,该数组内存在多个 wrapper 对象,每个对象包含两个属性:initialize
、close
。perform 中会先调用所有的 wrapper.initialize
,然后调用传入的回调,最后调用所有的 wrapper.close
。
batchingStrategy相关代码如下:
var transaction = new ReactDefaultBatchingStrategyTransaction();
var batchingStrategy = {
// 判断是否在更新流程中
isBatchingUpdates: false,
// 开始进行批量更新
batchedUpdates: function (callback, component) {
// 获取之前的更新状态
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
// 将更新状态修改为 true
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
// 如果已经在更新状态中,等待之前的更新结束
return callback(callback, component);
} else {
// 进行更新
return transaction.perform(callback, null, component);
}
}
};
class ReactDefaultBatchingStrategyTransaction extends Transaction {
constructor() {
this.reinitializeTransaction()
}
getTransactionWrappers () {
return [
{
initialize: () => {},
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
},
{
initialize: () => {},
close: () => {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
}
]
}
}
启动事务可以拆分成三步来看:
-
先执行 wrapper 的 initialize,此时的 initialize 都是一些空函数,可以直接跳过;
-
然后执行 callback(也就是 enqueueUpdate),执行 enqueueUpdate 时,由于已经进入了更新状态,
batchingStrategy.isBatchingUpdates
被修改成了true
,所以最后还是会把 component 放入脏组件队列,等待更新; -
后面执行的两个 close 方法,第一个方法的
flushBatchedUpdates
是用来进行组件更新的,第二个方法用来修改更新状态,表示更新已经结束。
其中flushBatchedUpdates
里面会取出所有的脏组件队列进行 diff,最后更新到 DOM。
function flushBatchedUpdates() {
if (dirtyComponents.length) {
runBatchedUpdates()
}
};
function runBatchedUpdates() {
// 省略了一些去重和排序的操作
for (var i = 0; i < dirtyComponents.length; i++) {
var component = dirtyComponents[i];
// 判断组件是否需要更新,然后进行 diff 操作,最后更新 DOM。
ReactReconciler.performUpdateIfNecessary(component);
}
}
performUpdateIfNecessary()
会调用 Component.updateComponent()
setState拿不到同步数据的原因
按照源码逻辑,setState 的时候,batchingStrategy.isBatchingUpdates
为 false
会开启一个事务,将组件放入脏组件队列,最后进行更新操作,而且这里都是同步操作。讲道理,setState 之后,我们可以立即拿到最新的 state。
然而,事实并非如此,在 React 的生命周期及其事件流中,batchingStrategy.isBatchingUpdates
的值早就被修改成了 true
在组件 mount 和事件调用的时候,都会调用 batchedUpdates
,这个时候已经开始了事务,所以只要不脱离 React,不管多少次 setState 都会把其组件放入脏组件队列等待更新。一旦脱离 React 的管理,比如在 setTimeout 中,setState 就可以马上拿到对应的更新后数据了。
如何立马获取setState更新后的数据
-
使用setTimeout,通过setTimeout去摆脱React的事务管理
componentDidMount(){ setTimeout(() => { this.setState({value:this.state.value+1}); console.log(this.state.value); this.setState({value:this.state.value+1}); console.log(this.state.value); this.setState({value:this.state.value+1}); console.log(this.state.value); this.setState({value:this.state.value+1}); console.log(this.state.value); },0) }
-
使用setState的回调函数,即传入第二个参数
this.setState(({value}=>{ value:value+1 }),()=>{ console.log(this.state.value); });
-
使用Promise,通过Promise去将对应的setState操作包起来,然后使用then去获取数据
setStatePromise(updator) { return new Promise( function (resolve,reject){ this.setState(updator,resolve); }.bind(this)) } componentDidMount(){ this.setStatePromise(({value}) => ({ value:value+1 })).then(() => { console.log(this.state.value); }); }
对于React16中的useState
我们通过例子来看看
function Component() {
const [a, setA] = useState(1)
const [b, setB] = useState('b')
console.log('render')
function handleClickWithPromise() {
Promise.resolve().then(() => {
setA((a) => a + 1)
setB('bb')
})
}
function handleClickWithoutPromise() {
setA((a) => a + 1)
setB('bb')
}
return (
<Fragment>
<button onClick={handleClickWithPromise}>
{a}-{b} 异步执行
</button>
<button onClick={handleClickWithoutPromise}>
{a}-{b} 同步执行
</button>
</Fragment>
)
}
点击同步执行时,只render了一次
点击异步执行时,render了两次
上面的情况是连续执行两个useState
我们来看看连续执行两次同一个useState看看
function Component() {
const [a, setA] = useState(1)
console.log('a', a)
function handleClickWithPromise() {
Promise.resolve().then(() => {
setA((a) => a + 1)
setA((a) => a + 1)
})
}
function handleClickWithoutPromise() {
setA((a) => a + 1)
setA((a) => a + 1)
}
return (
<Fragment>
<button onClick={handleClickWithPromise}>{a} 异步执行</button>
<button onClick={handleClickWithoutPromise}>{a} 同步执行</button>
</Fragment>
)
}
点击同步执行时,只render了一次,但是打印了3,证明两次setA都执行
点击异步执行时,render了两次,两次setA各自render了一次,分别打印了2,3
Tip:同步执行时useState也会对state进行逐个处理,而setState则只会处理最后一次
class Component extends React.Component {
constructor(props) {
super(props)
this.state = {
a: 1,
}
}
handleClickWithPromise = () => {
Promise.resolve().then(() => {
this.setState({a: this.state.a + 1})
this.setState({a: this.state.a + 1})
})
}
handleClickWithoutPromise = () => {
this.setState({a: this.state.a + 1})
this.setState({a: this.state.a + 1})
}
render() {
console.log('a', this.state.a)
return (
<Fragment>
<button onClick={this.handleClickWithPromise}>异步执行</button>
<button onClick={this.handleClickWithoutPromise}>同步执行</button>
</Fragment>
)
}
}
当点击同步执行
按钮时,两次 setState 合并,只执行了最后一次,打印 2
当点击异步执行
按钮时,两次 setState 各自 render 一次,分别打印 2,3setState到底是异步还是同步的呢