上篇: https://segmentfault.com/a/11...
在React中,若是是由React引起的事件处理(好比经过onClick引起的合成事件处理)和组件生命周期函数内(好比componentDidMount),调用this.setState不会同步更新this.state,除此以外的setState调用会同步执行this.state。
所谓“除此以外”,指的是绕过React经过addEventListener直接添加的事件处理函数,还有经过setTimeout/setInterval产生的异步调用。react
若是咱们按照教科书般的方式来使用React,基本上不会触及所谓的“除此以外”状况。
在React的setState函数实现中,会根据一个变量isBatchingUpdates
判断是直接更新this.state仍是放到队列中回头再说,而isBatchingUpdates
默认是false
,也就表示setState会同步更新this.state,可是,有一个函数batchedUpdates
,这个函数会把isBatchingUpdates
修改成true,而当React在调用事件处理函数和自身生命周期以前就会调用这个batchedUpdates
,形成的后果,就是由React控制的事件处理过程和生命周期中的同步代码调用
的setState
不会同步更新this.state
。webpack
注意:同步代码调用,在合成事件和生命周期内的异步调用setState(好比ajax和setTimeout内),也是会同步更新this.setState。
Demo请看上篇的Part Fourgit
因此按照正常React用法都是会通过batchingUpdate方法的。这是因为React有一套自定义的事件系统和生命周期流程控制,使用原生事件监听和settimeout这种方式会跳出React这个体系,因此会直接更新this.state。github
咱们在看代码是如何实现的,须要了解这样一个东西“事务”,React内部的工具方法实现了一个可供使用的事务。web
React中的事务借用了计算机专业术语的单词Transaction
。对比数据库的事务性质,二者之间有共同点却又不是一回事,简答来讲 把须要执行的方法用一个容器封装起来,在容器内执行方法的先后,分别执行init方法和close方,其次来讲,一个容器能够包裹另外一个容器,这点又相似于洋葱模型。ajax
React的合成事件系统和生命周期就使用了React内部实现的事务,为其函数附加了先后两个相似npm脚本pre和post两个钩子的事件。数据库
这是一个npm srcipt
的例子:npm
"prebuild": "echo I run before the build script", "build": "cross-env NODE_ENV=production webpack", "postbuild": "echo I run after the build script" //用户执行npm run build就会实际执行 npm run prebuild && npm run build && npm run postbuild //所以能够在两个钩子里作一些准备工做和清理工做。
有过有兴趣,咱们来看一下如何简单使用事务:https://codesandbox.io/s/6xl5yrjvzzsegmentfault
var MyTransaction = function () { //... }; Object.assign(MyTransaction.prototype, Transaction.Mixin, { getTransactionWrappers: function () { return [ { initialize: function () { console.log("before method perform"); }, close: function () { console.log("after method perform"); } } ]; } }); var transaction = new MyTransaction(); transaction.reinitializeTransaction() var testMethod = function () { console.log("test"); }; transaction.perform(testMethod);
因此,咱们能够获得启发,React的事件系统和生命周期事务先后的钩子对isBatchingUpdates作了修改,其实就是在事务的前置pre内调用了batchedUpdates方法修改了变量为true,而后在后置钩子又置为false,而后发起真正的更新检测,而事务中异步方法运行时候,因为JavaScript的异步机制,异步方法(setTimeout等)其中的setState运行时候,同步的代码已经走完,后置钩子已经把isBatchingUpdates设为false,因此此时的setState会直接进入非批量更新模式,表如今咱们看来成为了同步SetState。数组
尝试在描述一下:整个React的每一个生命周期和合成事件都处在一个大的事务当中。原生绑定事件和setTimeout异步的函数没有进入React的事务当中,或者是当他们执行时,刚刚的事务已经结束了,后置钩子触发了,close了。(你们能够想想分别是哪种状况)。
React“坐”在顶部调用堆栈框架并知道全部React事件处理程序什么时候运行,setState在React管理的合成事件或者生命周期中调用,它会启用批量更新事务,进入了批量更新模式,全部的setState的改变都会暂存到一个队列,延迟到事务结束再合并更新。若是setState在React的批量更新事务外部或者以后调用,则会当即刷新。
懂得了事务,再回看,就明白,其实setState历来都是同步运行,不过是React利用事务工具方法模拟了setState异步的假象。
延迟队列如何实现,其实有悟性的同窗已经能够大概猜到。咱们再来捋一捋,看源码是否能验证咱们的结论描述和现象。
能够对照这张图先来看下setState的流程代码,代码仓库在个人github的react-source仓库,目录已被我精简,剩下关键的源代码文件夹。
首先,咱们搜索setState =
看下setState何处被赋值,找到了这里
// src/isomorphic/modern/class/ReactComponent.js /* * React组件继承自React.Component,而setState是React.Component的方法, * 所以对于组件来说setState属于其原型方法,首先看setState的定义: */ ReactComponent.prototype.setState = function(partialState, callback) { // 忽略掉入参验证和开发抛错 //调用setState实际是调用了enqueueSetState // 调用队列的入队方法,把当前组件的示例和state存进入 this.updater.enqueueSetState(this, partialState); if (callback) { // 若是有回调,把回调存进setState队列的后置钩子 this.updater.enqueueCallback(this, callback, 'setState'); } };
会发现调用setState实际是调用this.updater.enqueueSetState,此时咱们不得不看一看updater及其enqueueSetState方法是什么东西,咱们在当前文件搜索:
function ReactComponent(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; // updater有默认值,真实运行时会注入,其实也算依赖注入 this.updater = updater || ReactNoopUpdateQueue; }
ReactNoopUpdateQueue是一个这样的对象,提供了基本的无效方法,真正的updater只有在React被真正加载前才会被注入进来,运行时注入,严格来讲是依赖注入,是React源码的风格之一。
// src/isomorphic/modern/class/ReactNoopUpdateQueue.js var ReactNoopUpdateQueue={ isMounted: function(publicInstance) { return false; }, enqueueCallback: function(publicInstance, callback) { }, enqueueForceUpdate: function(publicInstance) { }, enqueueReplaceState: function(publicInstance, completeState) { }, enqueueSetState: function(publicInstance, partialState) { }, }
真实的enqueueSetState在这个文件内,方法把将要修改的state存入组件实例的internalInstance数组中,这里就是state的延迟更新队列了。而后立马调用了一个全局的ReactUpdates.enqueueUpdate(internalInstance)方法。
// src/renderers/shared/reconciler/ReactUpdateQueue.js // 这个是setState真正调用的函数 enqueueSetState: function(publicInstance, partialState) { // 忽略基本的容错和抛错 // 存入组件实例,准备更新 var internalInstance = publicInstance; // 更新队列合并操做 更新 internalInstance._pendingStateQueue var queue = internalInstance._pendingStateQueue ||(internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); // 发生了什么?猜一下? ReactUpdates.js },
题外话:每一个函数的健壮入参判断和运行环境判断和完善的抛错机制,对来源不信任甚至是内部互调,是React对开发者友好的一个重要缘由。
咱们来猜下ReactUpdates.enqueueUpdate干了什么?根据上面的流程图我猜测应当是判断流程。
function enqueueUpdate(component) { ensureInjected(); //环境判断:是否有调度事务方法同时有批量更新策略方法 //关键的判断条件,是不是批量更新 //但是isBatchingUpdates这个值谁来维护呢? if (!batchingStrategy.isBatchingUpdates) { // 见词知意 若是不在批量更新策略中 // 若是不是批量更新,猜测一下,应该会当即更新吧? // 唉?batchingStrategy到底在作什么呢 batchingStrategy.batchedUpdates(enqueueUpdate, component); // 调用事务 // 对队列中的更新执行 batchedUpdates 方法 return; } // 若是是批量更新,那就把组件放入脏组件队列,也就是待更新组件队列 dirtyComponents.push(component); }
须要看ReactDefaultBatchingStrategy.js 看 batchedUpdates 方法,这个js文件就有意思了,一上来就是咱们以前提到的事务。
避免枯燥,我用人话阐述一下这个js的内容,也能够直接看ReactDefaultBatchingStrategy.js
var ReactDefaultBatchingStrategy={ isBatchingUpdates:false, batchedUpdates:function(){}, }
文件底部声明了 ReactDefaultBatchingStrategy对象,内部isBatchingUpdates初始值为false,这个就是咱们心心念念判断是否在批量更新策略的重要变量。
这个isBatchingUpdates变量搜索整个项目,发现它只被两处改变:
实质上,isBatchingUpdates仅仅也就是被batchedUpdates方法维护着,batchedUpdates调用时开启批量更新,同时入参callback被事务包裹调用,callback调用完成时候事务close钩子触发,关闭批量更新模式。事务的close钩子函数有两个,另外一个以前会调用ReactUpdates.flushBatchedUpdates方法,也就是真正的把积攒的setState队列进行更新计算。
问题来了,callback是啥,batchedUpdates方法在setState以前,或者说除了setState还会被谁调用,致使isBatchingUpdates变为true,我猜测是生命周期函数和合成事件,只有这样,整个维护批量更新策略的机制就造成了闭环,验证了咱们以前的结论。
咱们搜索batchedUpdates(
,果不其然,在src/renderers/dom/client/ReactEventListener.js
和src/renderers/dom/client/ReactMount.js
中找到了ReactUpdates.batchedUpdates的调用。
合成事件和生命周期的装载发生时,调用了batchedUpdates
方法,使得内部的同步代码均可以运行在批量更新策略的事务环境中,结束后,便使用事务的后置钩子启动merge更新,重置常量。
另外我在ReactDOM.js
发现了对React顶层API对batchedUpdates方法的引用,可让 Promise 这些异步也能进入 batch update:
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
另外一个彩蛋,虽然React不提倡使用这个API,之后版本也可能移除,可是如今咱们能够这样在React中这样使用:
React.unstable_batchedUpdates(function(){ this.setState({...}) this.setState({...}) //...在此函数内也可使用批量更新策略 })
解决了setTimeout和AJAX异步方法、原生事件内的setState批量更新策略失效的问题,让批量更新在任何场景都会发生。