上个月发表了一篇 React源码学习——ReactClass,但是后来我发现,大家对这种大量贴代码分析源码的形式并不感冒。讲道理,我自己看着也烦,还不如自己直接去翻源码来得痛快。吸取了上一次的教训,这次我决定:理性贴代码!翻阅源代码的工作还是留给各位小伙伴自己去做比较好。本来这次想准备说一说我们平时一直提到的React Virture DOM,但这可能又会造成无限贴源码的后果,因为virture dom在React中主要就是一个对象,在ReactElement中定义的,感兴趣的同学去源码中搜索一下createElement方法,就能看到virture dom是啥东西了。对其本身是没啥好说的,需要分析的应该是其在组件挂载和更新时的应用,因此对于ReactElement本身就不单独拿出来讲了,大家感兴趣就去翻阅一下源码吧。
进入正题
这次主要是要分析一下React中常见的setState方法,熟悉React的小伙伴应该都知道,该方法通常用于改变组件状态并用新的state去更新组件。但是,这个方法在很多地方的表现总是与我们的预期不符,先来看几个案例。
案例一
1 class Root extends React.Component { 2 constructor(props) { 3 super(props); 4 this.state = { 5 count: 0 6 }; 7 } 8 componentDidMount() { 9 let me = this; 10 me.setState({ 11 count: me.state.count + 1 12 }); 13 console.log(me.state.count); // 打印出0 14 me.setState({ 15 count: me.state.count + 1 16 }); 17 console.log(me.state.count); // 打印出0 18 setTimeout(function(){ 19 me.setState({ 20 count: me.state.count + 1 21 }); 22 console.log(me.state.count); // 打印出2 23 }, 0); 24 setTimeout(function(){ 25 me.setState({ 26 count: me.state.count + 1 27 }); 28 console.log(me.state.count); // 打印出3 29 }, 0); 30 } 31 render() { 32 return ( 33 <h1>{this.state.count}</h1> 34 ) 35 } 36 }
这个案例大家可能在别的地方中也见到过,结果确实让人匪夷所思,打印出0,0,2,3。先抛出两个问题:
- 为什么不在setTimeout中执行的两次setState均打印出0?
- 为什么setTimeout中执行的两次setState会打印出不同结果?
带着两个问题往下看。
React中的transaction(事务)
说到事务,我第一反应就是在以前使用sql server时用来处理批量操作的一个机制。当所有操作均执行成功,即可以commit transaction;若有一个操作失败,则执行rollback。在React中,也实现了一种类似的事务机制,其他文章也有详细的介绍。按照我个人的理解,React中一个事务其实就是按顺序调用一系列函数。在React中就是调用perform方法进入一个事务,该方法中会传入一个method参数。执行perform时先执行initializeAll方法按顺序执行一系列initialize的操作,例如一些初始化操作等等,然后执行传入的method,method执行完后就执行closeAll方法按顺序执行一系列close操作,例如下面会提到的执行批量更新或者将isBatchingUpdates变回false等等,然后结束这次事务。React中内置了很多种事务,注意,同一种事务不能同时开启,否则会抛出异常。我们还是回到我们上面的案例中来说明这个过程。
组件在调用ReactDOM.render()之后,会执行一个_renderNewRootComponent方法,大家可以去翻阅源码看一看,该方法执行了一个ReactUpdates.batchedUpdates()。batchedUpdates是什么呢?我们看看它的代码。
1 var transaction = new ReactDefaultBatchingStrategyTransaction(); 2 3 var ReactDefaultBatchingStrategy = { 4 isBatchingUpdates: false, 5 6 /** 7 * Call the provided function in a context within which calls to `setState` 8 * and friends are batched such that components aren't updated unnecessarily. 9 */ 10 batchedUpdates: function (callback, a, b, c, d, e) { 11 var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; 12 13 ReactDefaultBatchingStrategy.isBatchingUpdates = true; 14 15 // The code is written this way to avoid extra allocations 16 if (alreadyBatchingUpdates) { 17 return callback(a, b, c, d, e); 18 } else { 19 return transaction.perform(callback, null, a, b, c, d, e); 20 } 21 } 22 };
从代码中我们可以看出,这个batchedUpdates由于是第一次被调用,alreadyBatchingUpdates为false,因此会去执行transaction.perform(method),这就将进入一个事务,这个事务具体做了啥我们暂时不用管,我们只需要知道这个transaction是ReactDefaultBatchingStrategyTransaction的实例,它代表了其中一类事务的执行。然后会在该事务中调用perform中传入的method方法,即开启了组件的首次装载。当装载完毕会调用componentDidMount(注意,此时还是在执行method方法,事务还没结束,事务只有在执行完method后执行一系列close才会结束),在该方法中,我们调用了setState,出现了一系列奇怪的现象。因此,我们再来看看setState方法,这里只贴部分代码。
1 ReactComponent.prototype.setState = function (partialState, callback) { 2 !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0; 3 this.updater.enqueueSetState(this, partialState); 4 if (callback) { 5 this.updater.enqueueCallback(this, callback, 'setState'); 6 } 7 };
setState在调用时做了两件事,第一,调用enqueueSetState。该方法将我们传入的partialState添加到一个叫做_pendingStateQueue的队列中去存起来,然后执行一个enqueueUpdate方法。第二,如果存在callback就调用enqueueCallback将其存入一个_pendingCallbacks队列中存起来。然后我们来看enqueueUpdate方法。
1 function enqueueUpdate(component) { 2 ensureInjected(); 3 4 // Various parts of our code (such as ReactCompositeComponent's 5 // _renderValidatedComponent) assume that calls to render aren't nested; 6 // verify that that's the case. (This is called by each top-level update 7 // function, like setState, forceUpdate, etc.; creation and 8 // destruction of top-level components is guarded in ReactMount.) 9 10 if (!batchingStrategy.isBatchingUpdates) { 11 batchingStrategy.batchedUpdates(enqueueUpdate, component); 12 return; 13 } 14 15 dirtyComponents.push(component); 16 if (component._updateBatchNumber == null) { 17 component._updateBatchNumber = updateBatchNumber + 1; 18 } 19 }
是否看到了某些熟悉的字眼,如isBatchingUpdates和batchedUpdates。不错,其实翻阅代码就能明白,这个batchingStrategy就是上面的ReactDefaultBatchingStrategy,只是它通过inject的形式对其进行赋值,比较隐蔽。因此,我们当前的setState已经处于了这一类事务之中,isBatchingUpdates已经被置为true,所以将会把它添加到dirtyComponents中,在某一时刻做批量更新。因此在前两个setState中,并没有做任何状态更新,以及组件更新的事,而仅仅是将新的state和该组件存在了队列之中,因此两次都会打印出0,我们之前的第一个问题就解决了,还有一个问题,我们接着往下走。
在setTimeout中执行的setState打印出了2和3,有了前面的铺垫,我们大概就能得出结论,这应该就是因为这两次setState分别执行了一次完整的事务,导致state被直接更新而造成的结果。那么问题来了,为什么setTimeout中的setState会分别执行两次不同的事务?之前执行ReactDOM.render开启的事务在什么时候结束了?我们来看下列代码。
1 var RESET_BATCHED_UPDATES = { 2 initialize: emptyFunction, 3 close: function () { 4 ReactDefaultBatchingStrategy.isBatchingUpdates = false; 5 } 6 }; 7 8 var FLUSH_BATCHED_UPDATES = { 9 initialize: emptyFunction, 10 close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates) 11 }; 12 13 var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; 14 15 function ReactDefaultBatchingStrategyTransaction() { 16 this.reinitializeTransaction(); 17 } 18 19 _assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, { 20 getTransactionWrappers: function () { 21 return TRANSACTION_WRAPPERS; 22 } 23 });
这段代码也是写在ReactDefaultBatchingStrategy这个对象中的。我们之前提到这个事务中transaction是ReactDefaultBatchingStrategyTransaction的实例,这段代码其实就是给该事务添加了两个在事务结束时会被调用的close方法。即在perform中的method执行完毕后,会按照这里数组的顺序[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]依次调用其close方法。FLUSH_BATCHED_UPDATES是执行批量更新操作。RESET_BATCHED_UPDATES我们可以看到将isBatchingUpdates变回false,即意味着事务结束。接下来再调用setState时,enqueueUpdate不会再将其添加到dirtyComponents中,而是执行batchingStrategy.batchedUpdates(enqueueUpdate, component)开启一个新事务。但是需要注意,这里传入的参数是enqueueUpdate,即perform中执行的method为enqueueUpdate,而再次调用该enqueueUpdate方法会去执行dirtyComponents那一步。这就可以理解为,处于单独事务的setState也是通过将组件添加到dirtyComponents来完成更新的,只不过这里是在enqueueUpdate执行完毕后立即执行相应的close方法完成更新,而前面两个setState需在整个组件装载完成之后,即在componentDidMount执行完毕后才会去调用close完成更新。总结一下4个setState执行的过程就是:先执行两次console.log,然后执行批量更新,再执行setState直接更新,执行console.log,最后再执行setState直接更新,再执行console.log,所以就会得出0,0,2,3。
案例二
如下两种相似的写法,得出不同的结果。
class Root extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { let me = this; me.setState({ count: me.state.count + 1 }); me.setState({ count: me.state.count + 1 }); } render() { return ( <h1>{this.state.count}</h1> //页面中将打印出1 ) } }