从众所周知的setState小秘密说起……在React组件中,调用this.setState()是最基本的场景。该方法描述状态变化并触发组件重新渲染。然而,看似普普通通的this.setState()可能包含许多鲜为人知的设计和讨论。相信很多开发者已经意识到setState方法“maybe”是异步的。您可能认为更新状态似乎是一个微不足道的操作,没有什么是异步的。但要注意,因为状态更新会触发重新渲染,而重新渲染的代价是昂贵的,短时间内重复渲染从性能上来说是绝对不可取的。因此,React采用了批处理的思想,将一系列连续的状态更新进行批处理,只触发一次重新渲染。关于这些内容,如果你还不了解,推荐你参考@成颖的系列文章:setState:这个API是怎么设计的?:setState()门。或者,看看下面的一个小例子。例如,最简单的场景是:functionincrementMultiple(){this.setState({count:this.state.count+1});this.setState({count:this.state.count+1});这。setState({count:this.state.count+1});}直观上,当调用上面的incrementMultiple函数时,组件state的count值增加3次,每次增加1,最后增加count得到了3。但是,实际结果只是将state加1。不信你自己试试看~如果你想对几个让setState不断更新的hack一次加3,你应该如何优雅地处理潜在的异步操作,避免出现上述问题呢?下面提供了几种解决方案:方法一:一种常见的做法是将回调函数传递到setState方法中。这就是setState众所周知的功能用法。这确保即使在批处理更新时也能访问预期的状态或道具。(这样做的原理后面会有说明)方法二:另一种常见的做法是将setState更新后需要执行的逻辑(比如上面提到的第二次连续计数+1)封装成一个函数,作为第二个参数传递给setState。该功能逻辑将在更新后由React代理执行。即:setState(updater,[callback])方法三:将setState更新后需要执行的逻辑放在一个合适的生命周期钩子函数中,比如componentDidMount或者componentDidUpdate,当然也可以解决问题。也就是说第一次count+1后,启动componentDidUpdate生命周期钩子,第二次count+1操作可以直接放在componentDidUpdate函数中。看来曾经引起广泛讨论的一个Issue的内容已经不是什么新鲜事了。很多资深的React开发者其实都看懂了,或者说能很快看懂。但是,大家有没有想过这个问题:现代javascript处理异步过程,很流行的一种方式就是使用promises,那么我们是不是可以套用这个思路来解决呢?具体来说,调用setState方法后返回一个promise,state更新后调用promise.then进行下一步。答案是肯定的,但被官方拒绝了。我怎么会得出“答案是肯定的,但官方不推荐”。这个结论,喜欢问底线的读者请继续往下看,相信你会有所启发,也能更好地理解React团队的设计思想。Issue2642解读与深入分析在Facebook开源React的官方Github仓库上一步一步找到了蛛丝马迹。跟着整个过程,相信在各位高手的点评中,你会对React的设计理念和javascript的解题思路有更清晰的认识。所有的探索都始于React#2642issue:让setState返回一个promise,每个人都已经知道count+3inarow。下面我举一个实际在生产开发中的例子,方便大家理解和讨论。我们现在正在开发一个可编辑的表格。需求是:当用户按下回车键时,光标会进入下一行(调用setState移动光标);如果用户当前在最后一行,那么当用户回车时,第一步会先创建一个新行(调用setState创建一个新的最后一行),创建新行后,转到新的光标焦点的最后一行(调用setState移动光标)。常见错误的处理是:this.setState({selected:input//createnewline}.bind(this));this.props.didSelect(this.state.selected);因为第一个this.setState是异步完成的,当下一个didSelect方法执行this.setState时,处理的参数this.state.selected可能不是预期的下一行。很明显,这是this.setState的异步特性导致的问题。为了解决这个逻辑,我想到了setState第二个参数的解决方法,简单的用代码表示:this.setState({selected:input//createanewline},function(){this.props.didSelect(this.state.selected);}).bind(this));这个解决方案是使用嵌套的setState方法。但这无疑造成了嵌套地狱的问题。基于Promise的解决方案看起来像传统Javascript中处理异步的旧方法吗?解决回调地狱,有没有压力大的想到promise?如果setState方法返回promises会更优雅:setState()当前接受回调的可选第二个参数并返回undefined。这会导致非常有状态的组件出现回调地狱。让它返回一个承诺将使它更易于管理。如果使用promise风格来解决问题,无非就是:this.setState({selected:input}).then(function(){this.props.didSelect(this.state.selected);}.bind(这));外观精美,非常时尚的设计。但是,我们进一步思考:如果我们想让React支持这样的特性,我们应该如何通过提交pullrequest来更改源代码?探索React源码,完成setStatepromise的改造首先在源码中找到setState的定义,在react/src/isomorphic/modern/class/ReactBaseClasses.js目录下:ReactComponent.prototype.setState=function(partialState,callback){invariant(typeofpartialState==='object'||typeofpartialState==='function'||partialState==null,'setState(...):采用状态变量的对象来更新或一个'+'函数,它返回一个状态变量的对象。',);this.updater.enqueueSetState(this,partialState,callback,'setState');};我们首先看到一个注意事项:您可以提供一个可选的回调,该回调将在对setState的调用实际完成时执行。这是将setState的第二个参数传入处理回调的基础。另外,从评论中我们还发现:当一个函数被提供给setState时,它会在以后的某个时刻被调用(不是同步的)。它将使用最新的组件参数(state,props,context)被调用。这是将函数直接传递给setState方法的基础。言归正传,怎么改源码让setStatepromise呢?其实很简单,我只是把代码放上去:,'setState(...):采用状态变量对象进行更新或'+'返回状态变量对象的函数。',);+让回调承诺;+if(!callback){+classDeferred{+constructor(){+this.promise=newPromise((resolve,reject)=>{+this.reject=reject;+this.resolve=resolve;+});+}+}+callbackPromise=newDeferred();+callback=()=>{+callbackPromise.resolve();+};+}this.updater.enqueueSetState(this,partialState,callback,'setState');++if(callbackPromise){+returncallbackPromise.promise;+}};我用“+”标记了对源代码的更改。如果开发者调用setState方法,传入一个javascript对象,会返回一个promise,state更新后promise会resolve。不懂的建议补充一些基础知识,或者留言和我一起讨论。有解决方案,但是React会正式接受这个PR吗?不幸的是,答案是否定的。下面我们从React的设计思路和React官方团队的回应来了解一下拒绝的原因。sebmarkbage(Facebook工程师,React核心开发者)认为其实有很多方案可以解决异步带来的问题。比如我们可以在合适的生命周期钩子函数中完成相关逻辑。这种场景下,在row组件的componentDidMount中调用了focus,自然就完成了自动focus。另外还有一个方法:新的refs接口设计支持接收回调函数,当其子组件挂载时,回调函数会相应触发。以上所有模式都可以完全替代之前的问题解决方案,即使不能,也不代表接受对本次PR的承诺。为此,sebmarkbage说了一句很扎心的话:老实说,目前的批处理策略本身就存在一系列问题。在我们确定要保留当前模型之前,我对扩展它的API犹豫不决。在我们想出更好的办法之前,我认为这是一种暂时的逃避。问题的根源在于现有的批处理策略。老实说,这种策略带来了一系列的问题。也许后期会对此进行调整。在批处理策略没有调整之前,盲目扩展setState接口只会是一种短视的行为。对此,Redux的原作者DanAbramov也发表了自己的看法。他认为,根据他的经验,任何需要使用setState第二个参数回调的场景都可以使用生命周期函数componentDidUpdate(和/或componentDidMount)来覆盖。根据我的经验,每当我想使用setState回调时,我都可以通过覆盖componentDidUpdate(和/或componentDidMount)来实现相同的目的。另外,在一些极端的场景下,如果开发者确实需要同步处理的方式,比如如果我想在一个DOM元素挂载到屏幕之前做一些事情,promises是行不通的。因为Promises总是异步的。反之,如果setState支持这两种不同的方法,就显得完全没有必要和多余了。在社区中,确实有很多第三方库逐渐接受了promises风格,但是这些库解决的问题往往是强异步的,比如文件读取、网络操作等。React似乎没有必要添加这样一个令人困惑的功能。另外,如果每个setState都返回一个promise,也会对性能产生影响:对于React来说,setState必然会产生回调,这些回调需要妥善保存,以便在合适的时候触发。总结一下,有很多方法可以完美优雅的解决setState异步带来的问题。在这种情况下,让setState直接返回一个promise是多余的。此外,这会导致性能问题等。我个人认为这种思路很好,但难免有些Overengineering。这次为自己疯狂,我和我的倔强,你服了吗?如果不是,如果你不能更改React源代码,你只想使用基于promise的setState,你该怎么办?这里有一个“反模式”的解决方案:我们可以在不改变源代码的情况下自己修改它。原则上我们直接拦截this.setState,进行promises,然后再封装一个新的接口。从“bluebird”导入Promise;导出默认值{componentWillMount(){this.setStateAsync=Promise.promisify(this.setState);},};之后,您可以异步:this.setStateAsync({loading:true,})。然后(this.loadSomething).then((result)=>{returnthis.setStateAsync({result,loading:false});});当然你也可以使用原始的promise:functionsetStatePromise(that,newState){returnnewPromise((resolve)=>{that.setState(newState,()=>{resolve();});});}甚至...我们还可以大范围地使用async/await。最后,这一切都很脏,我不推荐。综上所述,其实如果你研究ReactIssue,深入研究源码,你会收获很多。总结没什么好说的,来打个不要脸的广告:我关于React的其他文章:ReactRedux中间件思想遇见WebWorker的灵感(附demo)通过实例,学习编写React组件的“最佳实践”React组件设计与分解思维【从React绑定本,看JS语言开发与框架设计】()React服务端渲染如此简单,从头开始搭建前后端应用,以及移动web版优步不足以创造极致性能。真章分析Twitter前端架构学习复杂场景数据设计ReactConf2017干货总结1:React+ESnext=?React+Redux打造“NEWSEARLY”单页应用A项目领悟前沿技术真相StackAReact+ReduxProjectExampleHappyCoding!PS:作者Github仓库和知乎问答链接,欢迎各种形式的交流。
