写业务代码的时候 须要常常用到setState, 前几天review代码的时候, 又想了一下这个API, 发现对它的了解不是很清楚, 仅仅是 setState
是异步的, 周六在家参考了一些资料,简单整理了下,写的比较简单, 通篇阅读大概耗时 5min, 在这简单分享一下, 但愿对你们有所帮助 ;)。html
假若有这样一个点击执行累加场景:前端
this.state = { count: 0, } incrementCount() { this.setState({ count: this.state.count + 1, }); } handleIncrement = () => { this.incrementCount(); this.incrementCount(); this.incrementCount(); }
每一次点击, 累加三次,看一下输入:react
并无达到预期的效果,纠正也很简单:数组
incrementCount() { this.setState((prevState) => { return {count: prevState.count + 1} }); }
再看输出:app
setState 的时候, 一个传入了object, 一个传入了更新函数。异步
区别在于: 传入一个更新函数
,就能够访问当前状态值。 setState调用是 批量处理
的,所以可让更新创建在彼此之上,避免冲突。函数
那问题来了, 为何前一种方式就不行呢? 带着这个疑问,继续往下看。性能
setState 为何不会同步更新组件
?this
进入这个问题以前,咱们先回顾一下如今对 setState 的认知:spa
不会马上改变
React组件中state的值.触发一次组件的更新
来引起重绘
.合并
。重绘指的就是引发 React 的更新生命周期函数4个函数:
shouldComponentUpdate
(被调用时this.state没有更新;若是返回了false,生命周期被中断,虽然不调用以后的函数了,可是state仍然会被更新)componentWillUpdate
(被调用时this.state没有更新)render
(被调用时this.state获得更新)componentDidUpdate
若是每一次 setState 调用都走一圈生命周期,光是想想也会以为会带来性能的问题,其实这四个函数都是纯函数,性能应该还好,可是render函数返回的结果会拿去作Virtual DOM比较和更新DOM树,这个就比较费时间。
目前React会将setState的效果放在队列
中,积攒着一次引起更新
过程。
为的就是把 Virtual DOM 和 DOM 树操做降到最小,用于提升性能
。
查阅一些资料后发现,某些操做仍是能够同步更新 this.state的。
setState 何时会执行同步更新
?
先直接说结论
吧:
在React中,若是是由React引起的事件处理(好比经过onClick引起的事件处理),调用 setState 不会同步更新 this.state,除此以外的setState调用会同步执行this.state。
所谓“除此以外”,指的是绕过React经过addEventListener
直接添加的事件处理函数,还有经过setTimeout
|| setInterval
产生的异步调用。
简单一点说, 就是通过React 处理的事件是不会同步更新
this.state的. 经过 addEventListener
|| setTimeou
t/setInterval
的方式处理的则会同步更新
。
具体能够参考 jsBin 的这个例子。
结果就很清晰了:
点击Increment ,执行onClick ,输出0;
而经过addEventListener , 和 setTimeout 方式处理的, 第一次 直接输出了1;
理论大概是这样的,盗用一张图:
在React的setState函数实现中,会根据一个变量 isBatchingUpdates
判断是直接更新
this.state 仍是放到队列
中。
而isBatchingUpdates
默认是false
,也就表示setState会同步更新
this.state,可是有一个函数batchedUpdates
。
这个函数会把isBatchingUpdates
修改成true
,而当React在调用事件处理函数以前就会调用这个batchedUpdates
,形成的后果,就是由React控制的事件处理过程setState不会同步更新
this.state。
经过上图,咱们知道了大体流程, 要想完全了解它的机制,咱们解读一下源码。
探秘setState 源码
// setState方法入口以下: ReactComponent.prototype.setState = function (partialState, callback) { // 将setState事务放入队列中 this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); }};
相关的几个概念:
partialState
,有部分state的含义,可见只是影响涉及到的state,不会伤及无辜。enqueueSetState
是 state 队列管理的入口方法,比较重要,咱们以后再接着分析。replaceState
:
replaceState: function (newState, callback) { this.updater.enqueueReplaceState(this, newState); if (callback) { this.updater.enqueueCallback(this, callback, 'replaceState'); }}, replaceState中取名为newState,有彻底替换的含义。一样也是以队列的形式来管理的。
enqueueSetState
enqueueSetState: function (publicInstance, partialState) { // 先获取ReactComponent组件对象 var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState'); if (!internalInstance) { return; } // 若是_pendingStateQueue为空,则建立它。能够发现队列是数组形式实现的 var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); // 将要更新的ReactComponent放入数组中 enqueueUpdate(internalInstance);}
其中getInternalInstanceReadyForUpdate
源码以下
function getInternalInstanceReadyForUpdate(publicInstance, callerName) { // 从map取出ReactComponent组件,还记得mountComponent时把ReactElement做为key,将ReactComponent存入了map中了吧,ReactComponent是React组件的核心,包含各类状态,数据和操做方法。而ReactElement则仅仅是一个数据类。 var internalInstance = ReactInstanceMap.get(publicInstance); if (!internalInstance) { return null; } return internalInstance;}
enqueueUpdate源码以下:
function enqueueUpdate(component) { ensureInjected(); // 若是不是正处于建立或更新组件阶段,则处理update事务 if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 若是正在建立或更新组件,则暂且先不处理update,只是将组件放在dirtyComponents数组中 dirtyComponents.push(component);}
batchedUpdates
batchedUpdates: function (callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; // 批处理最开始时,将isBatchingUpdates设为true,代表正在更新 ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations if (alreadyBatchingUpdates) { callback(a, b, c, d, e); } else { // 以事务的方式处理updates,后面详细分析transaction transaction.perform(callback, null, a, b, c, d, e); }} var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { // 事务批更新处理结束时,将isBatchingUpdates设为了false ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
enqueueUpdate
包含了React避免重复render
的逻辑。
mountComponent
和 updateComponent
方法在执行的最开始,会调用到 batchedUpdates
进行批处理
更新,此时会将isBatchingUpdates
设置为true
,也就是将状态标记为如今正处于更新阶段
了。
以后React以事务的方式处理组件update,事务处理完后会调用wrapper.close() 。
而TRANSACTION_WRAPPERS
中包含了RESET_BATCHED_UPDATES
这个wrapper
,故最终会调用RESET_BATCHED_UPDATES.close()
, 它最终会将isBatchingUpdates
设置为false
。
故 getInitialState
,componentWillMount
, render
,componentWillUpdate
中 setState
都不会引发 updateComponent
。
但在componentDidMount
和 componentDidUpdate
中则会。
事务经过wrapper
进行封装。
一个wrapper
包含一对 initialize
和 close
方法。好比 RESET_BATCHED_UPDATES
:
var RESET_BATCHED_UPDATES = { // 初始化调用 initialize: emptyFunction, // 事务执行完成,close时调用 close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};
transcation被包装在wrapper中,好比:
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
transaction
是经过transaction.perform(callback, args…)
方法进入的,它会先调用注册好的wrapper
中的initialize
方法,而后执行perform
方法中的callback
,最后再执行close
方法。
下面分析transaction.perform(callback, args…)
perform: function (method, scope, a, b, c, d, e, f) { var errorThrown; var ret; try { this._isInTransaction = true; errorThrown = true; // 先运行全部wrapper中的initialize方法 this.initializeAll(0); // 再执行perform方法传入的callback ret = method.call(scope, a, b, c, d, e, f); errorThrown = false; } finally { try { if (errorThrown) { // 最后运行wrapper中的close方法 try { this.closeAll(0); } catch (err) {} } else { // 最后运行wrapper中的close方法 this.closeAll(0); } } finally { this._isInTransaction = false; } } return ret; }, initializeAll: function (startIndex) { var transactionWrappers = this.transactionWrappers; // 遍历全部注册的wrapper for (var i = startIndex; i < transactionWrappers.length; i++) { var wrapper = transactionWrappers[i]; try { this.wrapperInitData[i] = Transaction.OBSERVED_ERROR; // 调用wrapper的initialize方法 this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null; } finally { if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) { try { this.initializeAll(i + 1); } catch (err) {} } } } }, closeAll: function (startIndex) { var transactionWrappers = this.transactionWrappers; // 遍历全部wrapper for (var i = startIndex; i < transactionWrappers.length; i++) { var wrapper = transactionWrappers[i]; var initData = this.wrapperInitData[i]; var errorThrown; try { errorThrown = true; if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) { // 调用wrapper的close方法,若是有的话 wrapper.close.call(this, initData); } errorThrown = false; } finally { if (errorThrown) { try { this.closeAll(i + 1); } catch (e) {} } } } this.wrapperInitData.length = 0; }
更新组件: runBatchedUpdates
前面分析到enqueueUpdate
中调用transaction.perform(callback, args...)
后,发现,callback
仍是enqueueUpdate
方法啊,那岂不是死循环
了?不是说好的setState
会调用updateComponent
,从而自动刷新View的吗? 咱们仍是要先从transaction事务提及。
咱们的wrapper中注册了两个wrapper,以下:
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
RESET_BATCHED_UPDATES
用来管理isBatchingUpdates
状态,咱们前面在分析setState是否当即生效时已经讲解过了。
那FLUSH_BATCHED_UPDATES
用来干吗呢?
var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)}; var flushBatchedUpdates = function () { // 循环遍历处理完全部dirtyComponents while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); // close前执行完runBatchedUpdates方法,这是关键 transaction.perform(runBatchedUpdates, null, transaction); ReactUpdatesFlushTransaction.release(transaction); } if (asapEnqueued) { asapEnqueued = false; var queue = asapCallbackQueue; asapCallbackQueue = CallbackQueue.getPooled(); queue.notifyAll(); CallbackQueue.release(queue); } }};
FLUSH_BATCHED_UPDATES
会在一个transaction
的close
阶段运行runBatchedUpdates
,从而执行update
。
function runBatchedUpdates(transaction) { var len = transaction.dirtyComponentsLength; dirtyComponents.sort(mountOrderComparator); for (var i = 0; i < len; i++) { // dirtyComponents中取出一个component var component = dirtyComponents[i]; // 取出dirtyComponent中的未执行的callback,下面就准备执行它了 var callbacks = component._pendingCallbacks; component._pendingCallbacks = null; var markerName; if (ReactFeatureFlags.logTopLevelRenders) { var namedComponent = component; if (component._currentElement.props === component._renderedComponent._currentElement) { namedComponent = component._renderedComponent; } } // 执行updateComponent ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction); // 执行dirtyComponent中以前未执行的callback if (callbacks) { for (var j = 0; j < callbacks.length; j++) { transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance()); } } }}
runBatchedUpdates
循环遍历dirtyComponents
数组,主要干两件事。
下面来看performUpdateIfNecessary
:
performUpdateIfNecessary: function (transaction) { if (this._pendingElement != null) { // receiveComponent会最终调用到updateComponent,从而刷新View ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context); } if (this._pendingStateQueue !== null || this._pendingForceUpdate) { // 执行updateComponent,从而刷新View。这个流程在React生命周期中讲解过 this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); } },
最后惊喜的看到了receiveComponent
和updateComponent
吧。
receiveComponent
最后会调用updateComponent
,而updateComponent
中会执行React组件存在期的生命周期方法,
如componentWillReceiveProps
, shouldComponentUpdate
, componentWillUpdate
,render
, componentDidUpdate
。
从而完成组件更新的整套流程。
总体流程回顾:
1.enqueueSetState将state放入队列中,并调用enqueueUpdate处理要更新的Component
2.若是组件当前正处于update事务中,则先将Component存入dirtyComponent中。不然调用batchedUpdates处理。
3.batchedUpdates发起一次transaction.perform()事务
4.开始执行事务初始化,运行,结束三个阶段
5.初始化:事务初始化阶段没有注册方法,故无方法要执行
6.运行:执行setSate时传入的callback方法,通常不会传callback参数
7.结束:更新isBatchingUpdates为false,并执行FLUSH_BATCHED_UPDATES这个wrapper中的close方法
8.FLUSH_BATCHED_UPDATES在close阶段,会循环遍历全部的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。
看完理论, 咱们再用一个例子巩固下.
class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log('第 1 次 log:', this.state.val); this.setState({val: this.state.val + 1}); console.log('第 2 次 log:', this.state.val); setTimeout(() => { this.setState({val: this.state.val + 1}); console.log('第 3 次 log:', this.state.val); this.setState({val: this.state.val + 1}); console.log('第 4 次 log:', this.state.val); }, 0); } render() { return null; } };
前两次在isBatchingUpdates
中,没有更新state, 输出两个0。
后面两次会同步更新, 分别输出2, 3;
很显然,咱们能够将4次setState简单规成两类
:
咱们先看看在componentDidMount中setState的调用栈:
再看看在setTimeOut中的调用栈:
咱们重点看看在componentDidMount中的sw3e调用栈 :
发现了batchedUpdates
方法。
原来在setState调用以前,就已经处于batchedUpdates执行的事务之中
了。
那batchedUpdates
方法是谁调用的呢?咱们再往上追溯一层,原来是ReactMount.js中的_renderNewRootComponent方法。
也就是说,整个将React组件渲染到DOM的过程就处于一个大的事务中了。
接下来就很容易理解了: 由于在componentDidMount
中调用setState
时,batchingStrategy
的isBatchingUpdates
已经被设置为true
,因此两次setState的结果并无当即生效,而是被放进了dirtyComponents
中。
这也解释了两次打印this.state.val都是0
的缘由,由于新的state还没被应用到组件中。
再看setTimeOut中的两次setState,由于没有前置的batchedUpdate
调用,因此batchingStrategy
的isBatchingUpdates
标志位是false
,也就致使了新的state
立刻生效,没有走到dirtyComponents
分支。
也就是说,setTimeOut中的第一次执行,setState时,this.state.val为1;
而setState完成后打印时this.state.val变成了2。
第二次的setState同理。
经过上面的例子,咱们就知道setState 是能够同步更新
的,可是仍是尽可能避免直接使用, 仅做了解就能够了。
若是你非要玩一些骚操做,写出这样的代码去直接去操做this.state:
this.state.count = this.state.count + 1; this.state.count = this.state.count + 1; this.state.count = this.state.count + 1; this.setState();
我只能说, 大胸弟, 你很骚。吾有旧友叼似汝,而今坟草丈许高。
最后简单重复下结论吧:
我对这一套理论也不是特别熟悉, 若有纰漏, 欢迎指正 :)
下面是个人公众号: 前端e进阶
有新内容会第一时间更新在这里, 但愿你们多多关注, 观看最新内容。
扩展阅读
https://reactjs.org/docs/faq-...
https://reactjs.org/docs/reac...
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...