React setState 源码解析
React setState
不知道什么时候开始,很多人开始认为setState是异步操作,所谓的异步操作,就是我们在执行了setState之后,立即通过this.state.xxx不能拿到更新之后的值。这样的认知其实有一种先入为主的意识,也许是受到很多不知名博主的不科学言论导致的错误认知,也有可能是日常开发过程中积累的经验。毕竟大部分开发写setState这样的方法,都是在组件的生命周期(如componentDidMount
、componentWillMount
)中,或者react的事件处理机制中,这种教科书式的写代码方式,基本不会碰到有数据异常。
虽然官方文档对setState这种同步行为语焉不详,但是我们可以发现某些情况下,setState是真的可以同步获取数据的。通过本文我们可以了解react这方面的工作原理,对于我们的思考开发方案,解决疑难问题,避免不必要的错误,也许会有不少帮助。
我们先来说结论:
在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理,componentWillMount等生命周期),调用setState不会同步更新this.state;除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。不想看长篇大论的同学,到这里就可以结束了。想了解原理的同学请继续参观。。
用过angular框架的同学也许记得angular的代码模式中有一个$timeout
这样的调用方法,和setTimeout
功能基本一致,但是setTimeout
却不能实时触发UI的更新。这是因为$timeout
比setTimeout
添加了对UI更新(脏检查)的处理,在延时结束后立即调用更新方法更新UI的渲染。同样的道理,我们必须使用react指定的方式更新state才能同步UI的渲染,因为react控制下的事件会同步处理UI的更新。而直接使用this.state.xxx = xxx
这样的方式仅仅改变了数据,没有改变UI,这就不是React倡导的reactive programing了。
实际上,在react的源码中我们会发现,大部分react控制下的事件或生命周期,会调用batchedUpdates
(查看如下代码)。这个方法会触发component渲染的状态isBatchingUpdates
。同样的,react的事件监听机制会触发batchedUpdates
方法,同样会将isBatchingUpdates
状态置为true。
// 更新状态 batchingStrategy.batchedUpdates(method, component);
在组件渲染状态isBatchingUpdates
中,任何的setState都不会触发更新,而是进入队列。除此之外,通过setTimeout/setInterval产生的异步调用是可以同步更新state的。这样的讲解比较抽象,我们可以直接根据以下源码开始理解。
setState
下面我们来看下setState在源码中的定义:
/** * Sets a subset of the state. Always use this to mutate * state. You should treat `this.state` as immutable. * * There is no guarantee that `this.state` will be immediately updated, so * accessing `this.state` after calling this method may return the old value. * * There is no guarantee that calls to `setState` will run synchronously, * as they may eventually be batched together. You can provide an optional * callback that will be executed when the call to setState is actually * completed. * * When a function is provided to setState, it will be called at some point in * the future (not synchronously). It will be called with the up to date * component arguments (state, props, context). These values can be different * from this.* because your function may be called after receiveProps but before * shouldComponentUpdate, and this new state, props, and context will not yet be * assigned to this. * * @param {object|function} partialState Next partial state or function to * produce next partial state to be merged with current state. * @param {?function} callback Called after state is updated. * @final * @protected */ ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } };
根据源码中的注释,有这么一句话。
There is no guarantee thatthis.state
will be immediately updated, so accessing this.state
after calling this method may return the old value.大概意思就是setState不能确保实时更新state,官方从来没有说过setState是一种异步操作,但也没有否认,只是告诉我们什么时候会触发同步操作,什么时候是异步操作。所以我们工作中千万不要被一些民间偏方蒙蔽双眼,多看看源代码,发现原理的同时,还可以发现很多好玩的东西,开源库的好处就是在于我们能在源码中发现真理。
我们在源码的这段注释里也能看到setState的一些有趣玩法,比如
// 在回调中操作更新后的state this.setState({ count: 1 }, function () { console.log('# next State', this.state); }); // 以非对象的形式操作 this.setState((state, props, context) => { return { count: state.count + 1 } });
回到正题,源码中setState执行了this.updater.enqueueSetState
方法和this.updater.enqueueCallback
方法 ,暂且不论enqueueCallback
,我们关注下enqueueSetState
的作用。
enqueueSetState
下面是enqueueSetState
的源码:
/** * Sets a subset of the state. This only exists because _pendingState is * internal. This provides a merging strategy that is not available to deep * properties which is confusing. TODO: Expose pendingState or don't use it * during the merge. * * @param {ReactClass} publicInstance The instance that should rerender. * @param {object} partialState Next partial state to be merged with state. * @internal */ enqueueSetState: function (publicInstance, partialState) { var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState'); if (!internalInstance) { return; } var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); }
enqueueSetState
如其名,是一个队列操作,将要变更的state统一插入队列,待一一处理。队列数据_pengdingStateQueue
会挂载在一个组件对象上internalInstance
,对于internalInstance
想要了解下的同学,可以参考下react源码中的ReactInstanceMap
这个概念。
队列操作完成之后,就开始真正的更新操作了。
enqueueUpdate
更新方法enqueueUpdate
的源码如下:
/** * Mark a component as needing a rerender, adding an optional callback to a * list of functions which will be executed once the rerender occurs. */ function enqueueUpdate(component) { ensureInjected(); // Various parts of our code (such as ReactCompositeComponent's // _renderValidatedComponent) assume that calls to render aren't nested; // verify that that's the case. (This is called by each top-level update // function, like setProps, setState, forceUpdate, etc.; creation and // destruction of top-level components is guarded in ReactMount.) if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); }
第一次执行setState的时候,可以进入if语句,遇到里面的return语句,终止执行。如果不是正处于创建或更新组件阶段,则处理update事务。
第二次执行setState的时候,进入不了if语句,将组件放入dirtyComponents。如果正在创建或更新组件,则暂且先不处理update,只是将组件放在dirtyComponents数组中。
enqueueUpdate
包含了React避免重复render的逻辑。参考源码中batchedUpdates
的调用情况,mountComponent
和updateComponent
方法在执行的最开始,会调用到batchedUpdates
进行批处理更新,这些是react实例的生命周期,此时会将isBatchingUpdates
设置为true,也就是将状态标记为现在正处于更新阶段了。之后React以事务的方式处理组件update,事务处理完后会调用wrapper.close()
, 而TRANSACTION_WRAPPERS
中包含了RESET_BATCHED_UPDATES
这个wrapper,故最终会调用RESET_BATCHED_UPDATES.close()
, 它最终会将isBatchingUpdates
设置为false。
听不懂?听不懂没关系。。我们会一句句剖析。
enqueueUpdate
和batchingStrategy
的概念我们放一起考虑。
batchingStrategy
简单直译叫做批量处理策略。这个是React处理批量state操作时的精髓,源码如下:
var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, /** * Call the provided function in a context within which calls to `setState` * and friends are batched such that components aren't updated unnecessarily. */ batchedUpdates: function (callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations if (alreadyBatchingUpdates) { callback(a, b, c, d, e); } else { transaction.perform(callback, null, a, b, c, d, e); } } };
如enqueueUpdate
源码中所述,每次执行更新前,会预先判断isBatchingUpdates
是否处理批量更新状态,如我们常见的周期诸如componentWillMount
、componentDidMount
,都是处于isBatchingUpdates
的批量更新状态,此时执行的setState操作,不会进入if语句执行update,而是进入dirtyComponents
的堆栈中。
这就是文章开头所说的栗子,为什么setTimeout执行的setState会同步更新state,而react生命周期中执行的setState只能异步更新的原因。只有react控制下的事件周期,会执行batchedUpdates
切换isBatchingUpdates
状态,保证批量操作能被截获并插入堆栈。其他事件都和同步执行update方法无异。
执行batchedUpdates
之后,会立即将isBatchingUpdates
赋值为true,表明此时即将进入更新状态,所有之后的setState进入队列等待。
这里我们以普通的setTimeout为例,执行一次更新。业务代如下:
setTimeout(function () { this.setState({ count: this.state.count + 1 }); }, 0);
执行时isBatchingUpdates
默认是false,所以当我们执行到batchedUpdates
这一步的时候,源码中alreadyBatchingUpdates
被赋值为false,我们会跳过if进入else条件,执行下一阶段transaction.perform
。
transaction.perform
perform
为我们执行了UI更新的第一步预操作。这里我们会执行一系列更新初始化操作和更新状态的关闭。该方法做了try-catch控制,大量数据操作有可能引发错误exception,perform方法在这里对错误做了截获控制。
/** * Executes the function within a safety window. Use this for the top level * methods that result in large amounts of computation/mutations that would * need to be safety checked. The optional arguments helps prevent the need * to bind in many cases. * * @param {function} method Member of scope to call. * @param {Object} scope Scope to invoke from. * @param {Object?=} a Argument to pass to the method. * @param {Object?=} b Argument to pass to the method. * @param {Object?=} c Argument to pass to the method. * @param {Object?=} d Argument to pass to the method. * @param {Object?=} e Argument to pass to the method. * @param {Object?=} f Argument to pass to the method. * * @return {*} Return value from `method`. */ perform: function (method, scope, a, b, c, d, e, f) { !!this.isInTransaction() ? "development" !== 'production' ? invariant(false, 'Transaction.perform(...): Cannot initialize a transaction when there ' + 'is already an outstanding transaction.') : invariant(false) : void 0; var errorThrown; var ret; try { this._isInTransaction = true; // Catching errors makes debugging more difficult, so we start with // errorThrown set to true before setting it to false after calling // close -- if it's still set to true in the finally block, it means // one of these calls threw. errorThrown = true; this.initializeAll(0); ret = method.call(scope, a, b, c, d, e, f); errorThrown = false; } finally { try { if (errorThrown) { // If `method` throws, prefer to show that stack trace over any thrown // by invoking `closeAll`. try { this.closeAll(0); } catch (err) {} } else { // Since `method` didn't throw, we don't want to silence the exception // here. this.closeAll(0); } } finally { this._isInTransaction = false; } } return ret; }
源码中执行了一些错误的预判,最终我们真正执行的是closeAll
方法。关于state的数据更新,从close开始。
close
/** * Invokes each of `this.transactionWrappers.close[i]` functions, passing into * them the respective return values of `this.transactionWrappers.init[i]` * (`close`rs that correspond to initializers that failed will not be * invoked). */ closeAll: function (startIndex) { !this.isInTransaction() ? "development" !== 'production' ? invariant(false, 'Transaction.closeAll(): Cannot close transaction when none are open.') : invariant(false) : void 0; var transactionWrappers = this.transactionWrappers; for (var i = startIndex; i < transactionWrappers.length; i++) { var wrapper = transactionWrappers[i]; var initData = this.wrapperInitData[i]; var errorThrown; try { // Catching errors makes debugging more difficult, so we start with // errorThrown set to true before setting it to false after calling // close -- if it's still set to true in the finally block, it means // wrapper.close threw. errorThrown = true; if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) { wrapper.close.call(this, initData); } errorThrown = false; } finally { if (errorThrown) { // The closer for wrapper i threw an error; close the remaining // wrappers but silence any exceptions from them to ensure that the // first error is the one to bubble up. try { this.closeAll(i + 1); } catch (e) {} } } } this.wrapperInitData.length = 0; }
在介绍close之前,我们先了解下两个对象。也就是源码中的this.transactionWrappers
。他在初始被赋值为[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]
,也就是以下两个对象,在源码被称作为wrapper
。
var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false; } }; var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates) };
源码中我们看到closeAll
执行了一次for循环,并执行了每个wrapper
的close
方法。
RESET_BATCHED_UPDATES
的close方法很简单,把isBatchingUpdates
更新中这个状态做了一个close的操作,也就是赋值为false,表明本次批量更新已结束。
FLUSH_BATCHED_UPDATES
的close方法执行的是flushBatchedUpdates
方法。
flushBatchedUpdates
var flushBatchedUpdates = function () { // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents // array and perform any updates enqueued by mount-ready handlers (i.e., // componentDidUpdate) but we need to check here too in order to catch // updates enqueued by setState callbacks and asap calls. while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); transaction.perform(runBatchedUpdates, null, transaction); ReactUpdatesFlushTransaction.release(transaction); } if (asapEnqueued) { asapEnqueued = false; var queue = asapCallbackQueue; asapCallbackQueue = CallbackQueue.getPooled(); queue.notifyAll(); CallbackQueue.release(queue); } } };
我们暂且不论asap是什么,可以看到flushBatchedUpdates
做的是对dirtyComponents
的批量处理操作,对于队列中的每个component执行perform更新。这些更新都会执行真正的更新方法runBatchedUpdates
。
function runBatchedUpdates(transaction) { var len = transaction.dirtyComponentsLength; !(len === dirtyComponents.length) ? "development" !== 'production' ? invariant(false, 'Expected flush transaction\'s stored dirty-components length (%s) to ' + 'match dirty-components array length (%s).', len, dirtyComponents.length) : invariant(false) : void 0; // Since reconciling a component higher in the owner hierarchy usually (not // always -- see shouldComponentUpdate()) will reconcile children, reconcile // them before their children by sorting the array. dirtyComponents.sort(mountOrderComparator); for (var i = 0; i < len; i++) { // If a component is unmounted before pending changes apply, it will still // be here, but we assume that it has cleared its _pendingCallbacks and // that performUpdateIfNecessary is a noop. var component = dirtyComponents[i]; // If performUpdateIfNecessary happens to enqueue any new updates, we // shouldn't execute the callbacks until the next render happens, so // stash the callbacks first var callbacks = component._pendingCallbacks; component._pendingCallbacks = null; var markerName; if (ReactFeatureFlags.logTopLevelRenders) { var namedComponent = component; // Duck type TopLevelWrapper. This is probably always true. if (component._currentElement.props === component._renderedComponent._currentElement) { namedComponent = component._renderedComponent; } markerName = 'React update: ' + namedComponent.getName(); console.time(markerName); } ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction); if (markerName) { console.timeEnd(markerName); } if (callbacks) { for (var j = 0; j < callbacks.length; j++) { transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance()); } } } }
runBatchedUpdates
中的核心处理是ReactReconciler.performUpdateIfNecessary
。
/** * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate` * is set, update the component. * * @param {ReactReconcileTransaction} transaction * @internal */ performUpdateIfNecessary: function (transaction) { if (this._pendingElement != null) { ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context); } if (this._pendingStateQueue !== null || this._pendingForceUpdate) { this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); } }
在这里我们终于又看到了我们熟悉的_pendingStateQueue
,还记得这是什么吗?是的,这就是state的更新队列,performUpdateIfNecessary
做了队列的特殊判断,避免导致错误更新。
接下来的这段代码是updateComponent
,源码内容比较长,但是我们可以看到很多熟知的生命周期方法的身影,比如说componentWillReceiveProps
和shouldComponentUpdate
,做了component的更新判断。
ReactCompositeComponentMixin
模块,有兴趣了解整个生命周期的同学可以参考下源码中的该模块源码,这里我们不再扩展,会继续讲解state的更新过程。updateComponent
/** * Perform an update to a mounted component. The componentWillReceiveProps and * shouldComponentUpdate methods are called, then (assuming the update isn't * skipped) the remaining update lifecycle methods are called and the DOM * representation is updated. * * By default, this implements React's rendering and reconciliation algorithm. * Sophisticated clients may wish to override this. * * @param {ReactReconcileTransaction} transaction * @param {ReactElement} prevParentElement * @param {ReactElement} nextParentElement * @internal * @overridable */ updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) { var inst = this._instance; var willReceive = false; var nextContext; var nextProps; // Determine if the context has changed or not if (this._context === nextUnmaskedContext) { nextContext = inst.context; } else { nextContext = this._processContext(nextUnmaskedContext); willReceive = true; } // Distinguish between a props update versus a simple state update if (prevParentElement === nextParentElement) { // Skip checking prop types again -- we don't read inst.props to avoid // warning for DOM component props in this upgrade nextProps = nextParentElement.props; } else { nextProps = this._processProps(nextParentElement.props); willReceive = true; } // An update here will schedule an update but immediately set // _pendingStateQueue which will ensure that any state updates gets // immediately reconciled instead of waiting for the next batch. if (willReceive && inst.componentWillReceiveProps) { inst.componentWillReceiveProps(nextProps, nextContext); } var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext); if ("development" !== 'production') { "development" !== 'production' ? warning(shouldUpdate !== undefined, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : void 0; } if (shouldUpdate) { this._pendingForceUpdate = false; // Will set `this.props`, `this.state` and `this.context`. this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext); } else { // If it's determined that a component should not update, we still want // to set props and state but we shortcut the rest of the update. this._currentElement = nextParentElement; this._context = nextUnmaskedContext; inst.props = nextProps; inst.state = nextState; inst.context = nextContext; } }
跳过除了state的其他源码部分,我们可以看到该方法中仍然嵌套了一段对state的更新方法,这个方法就是state更新的终点_processPendingState
。
_processPendingState
为什么对state中的同一属性做多次setState处理,不会得到多次更新?比如
this.setState({ count: count++ }); this.set
那是因为源码中的多个nextState的更新,只做了一次assign操作,如下源码请查看:
_processPendingState: function (props, context) { var inst = this._instance; 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; }
有人说,React抽象来说,就是一个公式
UI=f(state).
的确如此,一个简单的setState执行过程,内部暗藏了这么深的玄机,经历多个模块的处理,经历多个错误处理机制以及对数据边界的判断,保证了一次更新的正常进行。同时我们也发现了为什么setState的操作不能简单的说作是一个异步操作,大家应该在文章中已经找到了答案。
对其他react深层的理解,感兴趣的同学可以多多参考下源码。本文参考react源码版本为15.0.1
。