上个月发表了一篇 React源码学习——ReactClass,可是后来我发现,你们对这种大量贴代码分析源码的形式并不感冒。讲道理,我本身看着也烦,还不如本身直接去翻源码来得痛快。吸收了上一次的教训,此次我决定:理性贴代码!翻阅源代码的工做仍是留给各位小伙伴本身去作比较好。原本此次想准备说一说咱们平时一直提到的React Virture DOM,但这可能又会形成无限贴源码的后果,由于virture dom在React中主要就是一个对象,在ReactElement中定义的,感兴趣的同窗去源码中搜索一下createElement方法,就能看到virture dom是啥东西了。对其自己是没啥好说的,须要分析的应该是其在组件挂载和更新时的应用,所以对于ReactElement自己就不单独拿出来说了,你们感兴趣就去翻阅一下源码吧。html
此次主要是要分析一下React中常见的setState方法,熟悉React的小伙伴应该都知道,该方法一般用于改变组件状态并用新的state去更新组件。可是,这个方法在不少地方的表现老是与咱们的预期不符,先来看几个案例。sql
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。先抛出两个问题:数组
带着两个问题往下看。app
说到事务,我第一反应就是在之前使用sql server时用来处理批量操做的一个机制。当全部操做均执行成功,便可以commit transaction;如有一个操做失败,则执行rollback。在React中,也实现了一种相似的事务机制,其余文章也有详细的介绍。按照我我的的理解,React中一个事务其实就是按顺序调用一系列函数。在React中就是调用perform方法进入一个事务,该方法中会传入一个method参数。执行perform时先执行initializeAll方法按顺序执行一系列initialize的操做,例如一些初始化操做等等,而后执行传入的method,method执行完后就执行closeAll方法按顺序执行一系列close操做,例以下面会提到的执行批量更新或者将isBatchingUpdates变回false等等,而后结束此次事务。React中内置了不少种事务,注意,同一种事务不能同时开启,不然会抛出异常。咱们仍是回到咱们上面的案例中来讲明这个过程。dom
组件在调用ReactDOM.render()以后,会执行一个_renderNewRootComponent方法,你们能够去翻阅源码看一看,该方法执行了一个ReactUpdates.batchedUpdates()。batchedUpdates是什么呢?咱们看看它的代码。ide
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开启的事务在何时结束了?咱们来看下列代码。this
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 ) } }
class Root extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { let me = this; me.setState(function(state, props) { return { count: state.count + 1 } }); me.setState(function(state, props) { return { count: state.count + 1 } }); } render() { return ( <h1>{this.state.count}</h1> //页面中将打印出2 ) } }
这两种写法,一个是在setState中传入了object,一个是传入了function,却获得了两种不一样的结果,这是什么缘由形成的,这就须要咱们去深刻了解一下进行批量更行时都作了些什么。
前面提到事务即将结束时,会去调用FLUSH_BATCHED_UPDATES的flushBatchedUpdates方法执行批量更新,该方法会去遍历dirtyComponents,对每一项执行performUpdateIfNecessary方法,该方法代码以下:
1 performUpdateIfNecessary: function (transaction) { 2 if (this._pendingElement != null) { 3 ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context); 4 } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) { 5 this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); 6 } else { 7 this._updateBatchNumber = null; 8 } 9 }
在咱们的setState更新中,其实只会用到第二个 this._pendingStateQueue !== null 的判断,即若是_pendingStateQueue中还存在未处理的state,那就会执行updateComponent完成更新。那_pendingStateQueue是什么时候被处理的呢,继续看!
经过翻阅updateComponent方法,咱们能够知道_pendingStateQueue是在该方法中由_processPendingState(nextProps, nextContext)方法作了一些处理,该方法传入两个参数,新的props属性和新的上下文环境,这个上下文环境能够先不用管。咱们看看_processPendingState的具体实现。
1 _processPendingState: function (props, context) { 2 var inst = this._instance; // _instance保存了Constructor的实例,即经过ReactClass建立的组件的实例 3 var queue = this._pendingStateQueue; 4 var replace = this._pendingReplaceState; 5 this._pendingReplaceState = false; 6 this._pendingStateQueue = null; 7 8 if (!queue) { 9 return inst.state; 10 } 11 12 if (replace && queue.length === 1) { 13 return queue[0]; 14 } 15 16 var nextState = _assign({}, replace ? queue[0] : inst.state); 17 for (var i = replace ? 1 : 0; i < queue.length; i++) { 18 var partial = queue[i]; 19 _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial); 20 } 21 22 return nextState; 23 },
什么replace啊什么的均可以暂时不用看,主要先看for循环内部作的事情,replace咱们暂时认为是false。for循环遍历了_pendingStateQueue中全部保存的状态,对于每个状态进行处理,处理时首先判断保存的是function仍是object。如果function,就在inst的上下文中执行该匿名函数,该函数返回一个表明新state的object,而后执行assign将其与原有的state合并;如果object,则直接与state合并。注意,传入setState的第一个参数若是是function类型,咱们能够看到,其第一个参数nextState即表示更新以前的状态;第二个参数props表明更新以后的props,第三个context表明新的上下文环境。以后返回合并后的state。这里还须要注意一点,这一点很关键,代码中出现了this._pendingStateQueue = null这么一段,这也就意味着dirtyComponents进入下一次循环时,执行performUpdateIfNecessary不会再去更新组件,这就实现了批量更新,即只作一次更新操做,React在更新组件时就是用这种方式作了优化。
好了,回来看咱们的案例,当咱们传入函数做为setState的第一个参数时,咱们用该函数提供给咱们的state参数来访问组件的state。该state在代码中就对应nextState这个值,这个值在每一次for循环执行时都会对其进行合并,所以第二次执行setState,咱们在函数中访问的state就是第一次执行setState后已经合并过的值,因此会打印出2。然而直接经过this.state.count来访问,由于在执行对_pendingStateQueue的for循环时,组件的update还未执行完,this.state还未被赋予新的值,其实了解一下updateComponent会发现,this.state的更新会在_processPendingState执行完执行。因此两次setState取到的都是this.state.count最初的值0,这就解释了以前的现象。其实,这也是React为了解决这种先后state依赖可是state又没及时更新的一种方案,所以在使用时你们要根据实际状况来判断该用哪一种方式传参。
接下来咱们再来看看setState的第二个参数,回调函数,它是在何时执行的。
1 class Root extends React.Component { 2 constructor(props) { 3 super(props); 4 this.state = { 5 count: 0 6 }; 7 } 8 9 componentDidMount() { 10 let me = this; 11 setTimeout(function() { 12 me.setState({count: me.state.count + 1}, function() { 13 console.log('did callback'); 14 }); 15 console.log('hello'); 16 }, 0); 17 } 18 19 componentDidUpdate() { 20 console.log('did update'); 21 } 22 23 render() { 24 return <h1>{this.state.count}</h1> 25 } 26 }
这个案例控制台打印顺序是怎样的呢?不卖关子了,答案是did update,did callback,hello。这里是在一个setTimeout中执行了setState,所以其处于一个单独的事务之中,因此hello最后打印容易理解。而后咱们来看看setState执行更新时作了些啥。前面咱们知道在执行完组件装载即调用了componentDidMount以后,事务开始执行一系列close方法,这其中包括调用FLUSH_BATCHED_UPDATES中的flushBatchedUpdates,咱们来看看这段代码。
1 var flushBatchedUpdates = function () { 2 // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents 3 // array and perform any updates enqueued by mount-ready handlers (i.e., 4 // componentDidUpdate) but we need to check here too in order to catch 5 // updates enqueued by setState callbacks and asap calls. 6 while (dirtyComponents.length || asapEnqueued) { 7 if (dirtyComponents.length) { 8 var transaction = ReactUpdatesFlushTransaction.getPooled(); 9 transaction.perform(runBatchedUpdates, null, transaction); // 处理批量更新 10 ReactUpdatesFlushTransaction.release(transaction); 11 } 12 13 if (asapEnqueued) { 14 asapEnqueued = false; 15 var queue = asapCallbackQueue; 16 asapCallbackQueue = CallbackQueue.getPooled(); 17 queue.notifyAll(); // 处理callback 18 CallbackQueue.release(queue); 19 } 20 } 21 };
能够看我作了中文标注的两个地方,这个方法其实主要就是处理了组件的更新和callback的调用。组件的更新发生在runBatchedUpdates这个方法中,下面的queue.notifyAll内部其实就是从队列中去除callback调用,所以应该是先执行完更新,调用componentDidUpdate方法以后,再去执行callback,就有了咱们上面的结果。
React在组件更新方面作了不少优化,这其中就包括了上述的批量更新。在componentDidMount中执行了N个setState,若是执行N次更新是件很傻的事情。React利用其独特的事务实现,作了这些优化。正是由于这些优化,才形成了上面见到的怪现象。还有一点,再使用this.state时必定要注意组件的生命周期,不少时候在获取state的时候,组件更新还未完成,this.state还未改变,这是很容易形成bug的一个地方,要避免这个问题,须要对组件生命周期有必定的了解。在执行setState时,咱们能够经过在第一个参数传入function的形式来避免相似的问题。若是你们发现有任何问题,均可以在评论中告诉我,感激涕零。