在调用setState时,react会帮我们去更新DOM,重新去render一次,这个过程是怎么实现的呢?了解它就能很好的帮助我们理解为什么有时我们通过setState改变了一个变量的值,而再次使用时还是原来的值。
先来看个例子。
一个点击事件中调用setState
import React from 'react';
export default class RIndex extends React.Component {
state = {
text1: 1
}
constructor(props) {
super(props);
this.change = this.change.bind(this);
}
change() {
this.setState({
text1: this.state.text1 + 1
});
debugger;
}
render() {
return (
<div>
<h1>This is RIndex</h1>
<h3>{this.state.text1}</h3>
<button onClick={this.change}>change</button>
</div>
);
}
}
我们调用setState时,使用的是this,而这个this是当前react组件,当前react组件继承自React.Componet,所以setState应该是React.Component中定义的方法,找到源码中的React.js文件,一步步找下去会发现,setState的调用路径如下:
/** ====== React.js ====== */
...
Component: ReactBaseClasses.Component
...
/** ====== ReactBaseClasses ====== */
...
ReactComponent.prototype.setState = function(...) {
...
// this.updater是ReactUpdateQueue,找到这个对应关系很简单,只需要全局搜索enqueueSetState方法,
// 但要弄明白是在哪里赋值的,那就需要花很长时间了
this.updater.enqueueSetState(this, partialState);
...
}
...
/** ====== ReactUpdateQueue ======*/
...
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
...
enqueueSetState: function (publicInstance, partialState) {
enqueueUpdate(internalInstance);
}
...
/** ====== ReactUpdates ======*/
...
function enqueueUpdate(component) {}
...
查看调用路径中的方法,会发现,这个路径中,仅仅是将入参中的state放到一个实例的属性中,以及将当前组件放到ReactUpdates 的全局变量dirtyComponents中,并没有发现真正是在哪里去触发更新DOM的操作。回想整个流程,我们的setState是放在事件回调中的,那么事件触发时会不会做了什么事呢?
关注事件触发代码
找到事件触发的代码
/** ReactEventListener */
dispatchEvent: function (topLevelType, nativeEvent) {
...
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
...
}
发现在事件触发的方法中,有一个ReactUpdates.batchedUpdates方法的调用,看名字应该和更新DOM有关系,查看该方法的调用链路
/** ====== ReactUpdates ======*/
function batchedUpdates(callback, a, b, c, d, e) {
ensureInjected();
return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}
/** ====== ReactDefaultBatchingStrategy ======*/
batchedUpdates: function (callback, a, b, c, d, e) {
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}
其中batchingStrategy是在react组件初始化的时候注册进来的,指向ReactDefaultBatchingStrategy对象。ReactDefaultBatchingStrategy对象的batchedUpdates方法,第一个参数callback就是事件触发时传入的handleTopLevelImpl,是真正的事件回调函数,从这个if语句看,应该是若已经update过了,就直接触发callback,若没有update,则调用transaction.perform。
Transaction是react中定义的事务,Transaction文件中有说明以及一个很形象的图:
可以看出Transaction就是可以在你想要调用的方法之前及之后包入其他方法,且保证这些外层方法一定会执行。
查看ReactDefaultBatchingStrateg中的transaction的实现
/** ====== ReactDefaultBatchingStrategy ======*/
...
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
// 传入两个wrapper生成transaction
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
this.reinitializeTransaction();
}
_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function () {
return TRANSACTION_WRAPPERS;
}
});
var transaction = new ReactDefaultBatchingStrategyTransaction();
可以看到ReactDefaultBatchingStrategy中的transaction包了两个wrapper,第一个wrapper的close中传入了ReactUpdates.flushBatchedUpdates,也就是在事件触发后,会调用ReactUpdates.flushBatchedUpdates方法,而flushBatchedUpdates后续的代码才是正直触发了更新DOM的操作。
注:可以好好理解一下Transcation,react源码中大量使用了它。
总结
完整的调用流程为:事件触发 ---> 包装一个事务 ---> 事务在触发事件回调之后执行更新DOM操作。也就是若我们在事件回调中多次调用了setState,实际react是先把这些setState里设置的值全部放到当前组件的_pendingStateQueue对象中,最后再一次调用更新DOM的方法的。
因此,当我们在一个事件回调函数中写了两次setState时,实际只执行了一次更新DOM的操作。如下代码的结果也就不难理解了
state = {
count: 0
}
// 事件执行一次后,state中的count为1
change() {
// this.state.count在这里为0
this.setState({count: this.state.count + 1);
// this.state.count在这里也为0
this.setState({count: this.state.count + 1);
}