从 setState promise 化的探讨 体会 React 团队设计思想

从 setState 那个众所周知的小秘密说起...

在 React 组件中,调用 this.setState() 是最基本的场景。这个方法描述了 state 的变化、触发了组件 re-rendering。但是,也许看似平常的 this.setState() 里面却也许蕴含了很多鲜为人知的设计和讨论。

相信很多开发者已经意识到,setState 方法“或许”是异步的。也许你觉得,看上去更新 state 是如此轻而易举的操作,这并没有什么可异步处理的。但是要意识到,因为 state 的更新会触发 re-rendering,而 re-rendering 代价昂贵,短时间内反复进行渲染在性能上肯定是不可取的。所以,React 采用 batching 思想,它会 batches 一系列连续的 state 更新,而只触发一次 re-render。

关于这些内容,如果你还不清楚,推荐参考@程墨的系列文章:setState:这个API设计到底怎么样;英语好的话,可以直接关注长发飘飘的 Eric Elliott 著名的引起系列口水战的吐槽文:setState() Gate

或者,直接看下面的一个小例子。
比如,最简单的一个场景是:

function incrementMultiple() {
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
}

直观上来看,当上面的 incrementMultiple 函数被调用时,组件状态的
count 值被增加了3次,每次增加1,那最后 count 被增加了3。但是,实际上的结果只给 state 增加了1。不信你自己试试~

让 setState 连续更新的几个 hack

如果想让 count 一次性加3,应该如何优雅地处理潜在的异步操作,规避上述问题呢?

以下提供几种解决方案:

  • 方法一:常见的一种做法便是将一个回调函数传入 setState 方法中。即 setState 著名的函数式用法。这样能保证即便在更新被 batched 时,也能访问到预期的 state 或 props。(后面会解释这么做的原理)
  • 方法二:另外一个常见的做法是需要在 setState 更新之后进行的逻辑(比如上述的连续第二次 count + 1),封装到一个函数中,并作为第二个参数传给 setState。这段函数逻辑将会在更新后由 React 代理执行。即:

setState(updater, [callback])

  • 方法三:把需要在 setState 更新之后进行的逻辑放在一个合适的生命周期 hook 函数中,比如 componentDidMount 或者 componentDidUpdate 也当然可以解决问题。也就是说 count 第一次 +1 之后,出发 componentDidUpdate 生命周期 hook,第二次 count +1 操作直接放在 componentDidUpdate 函数里面就好啦。

一个引起广泛讨论的 Issue

这些内容貌似已经不再新鲜,很多 React 资深开发者其实都是了解的,或能很快理解。

可是,你想过这个问题吗:
现代 javascript 处理异步流程,很流行的一个做法是使用 promises,那么我们能否应用这个思路解决呢?

说具体一些,就是调用 setState 方法之后,返回一个 promise,状态更新完毕后我们在调用 promise.then 进行下一步处理。

答案是肯定的,但是却被官方否决了。

我是如何得出“答案是肯定的,但是是不被官方建议的。”这个结论,喜欢刨根问底的读者请继续往下阅读,相信你一定会有所启发,也能更充分理解 React 团队的设计思想。

第 2642 Issue 解读和深入分析

我是一步一步在 Facebook 开源 React 的官方 Github仓库上,找到了线索。

整个过程跟下来,相信在各路大神的 comments 之间,你会对 React 的设计理念以及 javascript 解决问题的思路有一个更清晰的认识。

一切的探究始于 React 第 #2642 号 issue: Make setState return a promise,上面关于 count 连续 +3 大家已经有所了解。接下来我举一个真正在生产开发中的例子,方便大家理解讨论。

我们现在开发一个可编辑的 table,需求是:当用户敲下“回车”,光标将会进入下一行(调用 setState 进行光标移动);如果用户当前已经在最后一行,那么敲下回车时,第一步将先创建一个新行(调用 setState 创建新的最后一行),在新行创建之后,再去新的最后一行进行光标聚焦(调用 setState 进行光标移动)。

常见且错误的处理在于:

this.setState({
  selected: input
  // 创建新行
}.bind(this));
this.props.didSelect(this.state.selected);

因为第一个 this.setState 是异步进行的话,下一处 didSelect 方法执行 this.setState 时,所处理的参数 this.state.selected 可能还不是预期的下一行。很明显,这就是 this.setState 的异步性带来的问题。

为了解决这个完成这样的逻辑,想到了 setState 第二个参数解决方案,用代码简单表述就是:

this.setState({
  selected: input
  // 创建新行
}, function() {
    this.props.didSelect(this.state.selected);
}).bind(this));

这种解决方案是使用嵌套的 setState 方法。但这无疑潜在地会带来嵌套地狱的问题。

Promise 化方案登场

这一切是不是像极了传统 Javascript 处理异步老套路?解决回调地狱,你是不是应激性地想到了 promise?

如果 setState 方法返回的是一个 promises,自然会更加优雅:

setState() currently accepts an optional second argument for callback and returns undefined.

This results in a callback hell for a very stateful component. Having it return a promise would make it much more managable.

如果用 promise 风格解决问题的话,无非就是:

this.setState({
  selected: input
}).then(function() {
  this.props.didSelect(this.state.selected);
}.bind(this));

看上去没什么问题,一个很时髦的设计。但是,我们进一步想:如果想让 React 支持这样的特性,采用提出 pull request 的方式,我们该如何去改源代码呢?

探索 React 源码,完成 setState promise 化的改造

首先找到源码中关于 setState 定义的地方,它在 react/src/isomorphic/modern/class/ReactBaseClasses.js 这个目录下:

ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === ‘object‘ ||
      typeof partialState === ‘function‘ ||
      partialState == null,
    ‘setState(...): takes an object of state variables to update or a ‘ +
      ‘function which returns an object of state variables.‘,
  );
  this.updater.enqueueSetState(this, partialState, callback, ‘setState‘);
};

我们首先看到一句注释:

You can provide an optional callback that will be executed when the call to setState is actually completed.

这是采用 setState 第二个参数传入处理回调的基础。

另外,从注释中我们还找到:

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).

这是给 setState 方法直接传入一个函数的基础。

言归正传,如何改动源码,使得 setState promise 化呢?
其实很简单,我直接上代码:

 ReactComponent.prototype.setState = function(partialState, callback) {
   invariant(
     typeof partialState === ‘object‘ ||
       typeof partialState === ‘function‘ ||
       partialState == null,
      ‘setState(...): takes an object of state variables to update or a ‘ +
        ‘function which returns an object of state variables.‘,
    );
 +  let callbackPromise;
 +  if (!callback) {
 +    class Deferred {
 +      constructor() {
 +        this.promise = new Promise((resolve, reject) => {
 +          this.reject = reject;
 +          this.resolve = resolve;
 +        });
 +      }
 +    }
 +    callbackPromise = new Deferred();
 +    callback = () => {
 +      callbackPromise.resolve();
 +    };
 +  }
    this.updater.enqueueSetState(this, partialState, callback, ‘setState‘);
 +
 +  if (callbackPromise) {
 +    return callbackPromise.promise;
 +  }
  };

我用 “+” 标注了对源码所做的更改。如果开发者调用 setState 方法时,传入的是一个 javascript 对象的话,那么会返回一个 promise,这个 promise 将会在 state 更新完毕后 resolve。
如果您看不懂的话,建议补充一下相关的基础知识,或者留言与我讨论。

解决方案有了,可是 React 官方会接受这个 PR 吗?

很遗憾,答案是否定的。我们来从 React 设计思想上,和 React 官方团队的回应上,了解一下否决理由。

sebmarkbage(Facebook 工程师,React 核心开发者)认为:解决异步带来的困扰方案其实很多。比如,我们可以在合适的生命周期 hook 函数中完成相关逻辑。在这个场景里,就是在行组件的 componentDidMount 里调用 focus,自然就完成了自动聚焦。

此外,还有一个方法:新的 refs 接口设计支持接收一个回调函数,当其子组件挂载时,这个回调函数就会相应触发。

所有上述模式都可以完全取代之前的问题方案,即使不能也不意味着要接受 promises 化这个PR。

为此,sebmarkbage 说了一段很扎心的话:

Honestly, the current batching strategy comes with a set of problems right now. I‘m hesitant to expand on it‘s API before we‘re sure that we‘re going to keep the current model. I think of it as a temporary escape until we figure out something better.

问题的根源在于现有的 batching 策略,实话实说,这个策略带来了一系列问题。也许这个在后期后有调整,在 batching 策略是否调整之前,盲目的扩充 setState 接口只会是一个短视的行为。

对此,Redux 原作者 Dan Abramov 也发表了自己的看法。他认为,以他的经验来看,任何需要使用 setState 第二个参数 callback 的场景,都可以使用生命周期函数 componentDidUpdate (and/or componentDidMount) 来复写。

In my experience, whenever I‘m tempted to use setState callback, I can achieve the same by overriding componentDidUpdate (and/or componentDidMount).

另外,在一些极端场景下,如果开发者确实需要同步的处理方式,比如如果我想在某 DOM 元素挂载到屏幕之前做一些操作,promises 这种方案便不可行。因为 Promises 总是异步的。反过来,如果 setState 支持这两种不同的方式,那么似乎也是完全没有必要而多余的。

在社区,确实很多第三方库渐渐地接受使用 promises 风格,但是这些库解决的问题往往都是强异步性的,比如文件读取、网络操作等等。 React 似乎没有必要增加这么一个 confusing 的特性。

另外,如果每个 setState 都返回一个 promises,也会带来性能影响:对于 React 来说,setState 将必然产生一个 callback,这些 callbacks 需要合理储存,以便在合适时间来触发。

总结一下,解决 setState 异步带来的问题,有很多方式能够完美优雅地解决。在这种情况下,直接让 setState 返回 promise 是画蛇添足的。另外,这样也会引起性能问题等等。

我个人认为,这样的思路很好,但是难免有些 Overengineering。

这一次为自己疯狂,我和我的倔强

怎么样,是否说服你了呢?如果没有,在不能更改 React 源码情况下,你就是想用 promise 化的 setState,怎么办呢?

这里提供一个“反模式”的方案:我们不改变源码,自己也可以进行改造,原理上就是直接对 this.setState 进行拦截,进而进行 promise 化,再封装一个新的接口出来。

import Promise from "bluebird";

export default {
  componentWillMount() {
    this.setStateAsync = Promise.promisify(this.setState);
  },
};

之后,便可以异步地:

this.setStateAsync({
  loading: true,
}).then(this.loadSomething).then((result) => {
  return this.setStateAsync({result, loading: false});
});

当然,也可以使用原声的 promises:

function setStatePromise(that, newState) {
    return new Promise((resolve) => {
        that.setState(newState, () => {
            resolve();
        });
    });
}

甚至...我们还可以脑洞大开使用 async/await。

最后,所有这种做法非常的 dirty,我是不建议这么使用的。

总结

其实研究一下 React Issue,深入源码学习,收获确实很多。总结也没有更多想说的了,无耻滴做个广告吧:

我的其他关于 React 文章:

Happy Coding!

PS:
作者Github仓库知乎问答链接
欢迎各种形式交流。

原文地址:https://www.cnblogs.com/jlfw/p/12211039.html

时间: 2024-07-30 02:53:10

从 setState promise 化的探讨 体会 React 团队设计思想的相关文章

React框架为什么这么火:React的设计思想

原文链接:http://mp.weixin.qq.com/s?__biz=MzA5Njc3Njk5NA==&mid=2650528748&idx=1&sn=4d3093e963ce76c642eb0c5d9a97625b#rd React的起源 React来自于Facebook,是的,就是那个你们听说过但是打不开的网站.Facebook的开发者当时在开发一个广告系统,因为对当前所有的MVC框架不满意,所以就自己写了一个UI框架,于是就有了React.后来因为觉得实在是好用,所以在2

React的设计思想——理解JSX和Component

基于HTML的前端界面开发正变得越来越复杂,其本质问题基本都可以归结于如何将来自于服务器端或者用户输入的动态数据高效的反映到复杂的用户界面上.而来自Facebook的React框架正是完全面向此问题的一个解决方案.React带来了很多开创性的思路来构建前端界面,虽然选择React的最重要原因之一是性能,但是相关技术背后的设计思想更值得我们去思考. React项目经理Tom Occhino曾经阐述React诞生的初衷,他提到React最大的价值究竟是什么?是高性能虚拟DOM.服务器端Render.

【笔记】HybridApp中使用Promise化的JS-Bridge

背景: HybridApp,前端采用JS-bridge的方式调用Native的接口,如获取设备信息.拍照.人脸识别等 前端封装了调用库,每次调用Native接口,需要进行两步操作(1.在window下挂载Native回调函数:2.调用InvokeNative函数,发送请求数据) 改造前: 使用回调,在每次调用Ygritte中的方法前,必须先定义好回调函数,挂载在window[funcName]上.然后调用. 问题: 1.可能导致回调地狱:比如某个场景中,需要先判断App版本,然后调用不同的Nat

React的设计哲学 - 简单之美

React最初来自Facebook内部的广告系统项目,项目实施过程中前端开发遇到了巨大挑战,代码变得越来越臃肿且混乱不堪,难以维护.于是痛定思痛,他们决定抛开很多所谓的“最佳实践”,重新思考前端界面的构建方式,于是就有了React. React带来了很多开创性的思路来构建前端界面,虽然选择React的最重要原因之一是性能,但是相关技术背后的设计思想更值得我们去思考.之前我也曾写过一篇React的入门文章,并提供了示例代码,大家可以结合参考. 上个月React发布了最新的0.13版,并提供了对ES

深入浅出React(一):React的设计哲学 - 简单之美

编者按:自2013年Facebook发布以来,React吸引了越来越多的开发者,基于它的衍生技术,如React Native.React Canvas等也层出不穷.InfoQ精心策划"深入浅出React"系列文章,为读者剖析React开发的技术细节. React最初来自Facebook内部的广告系统项目,项目实施过程中前端开发遇到了巨大挑战,代码变得越来越臃肿且混乱不堪,难以维护.于是痛定思痛,他们决定抛开很多所谓的"最佳实践",重新思考前端界面的构建方式,于是就有

React和设计良好的jQuery插件并没有什么不兼容的问题。

著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处.作者:fluxxu链接:http://www.zhihu.com/question/36045843/answer/67064767来源:知乎 React和设计良好的jQuery插件并没有什么不兼容的问题.我所说的设计良好的插件,就是你给他一个Element,他只在这个Element里面操作,不到处瞎搞.还有就是它提供了安全destroy的方法可以调用.一般是这样,比如你想用某个jQuery的autocomplete插件,在jQ

webUploader react 接口设计

import React, {PropTypes} from 'react'; export default class Test extends React.Component { constructor(props) { super(props); this.state = { webUploader:null, showDialog:false, selectFiles:[] }; this.uploadConfig={}; } componentDidMount(){ } uploade

jquery设计思想之写法-方法函数化&链式操作

1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <meta http-equiv="

简述组件化解决方案CTMediator与MGJRouter的主要思想

简述CTMediator CTMediator按照功能的结构来讲,使用时需要实现CTMediator的个三部分. 1.CTMediator类:承担总枢纽,总调度的责任 2.Target_(ModuleName)类:承担组件对外暴漏接口功能,组件要提供什么服务,主要在它的接口声明上进行体现 3.CTMediator+(ModuleName)分类:主要供客户端使用,里面声明了可以调用的组件接口. 下面详细讲解 Part1: CTMediator核心功能实现: CTMediator主要采用target