【问题标题】:In ReactJS, why does `setState` behave differently when called synchronously?在 ReactJS 中,为什么 `setState` 在同步调用时表现不同?
【发布时间】:2015-05-09 10:40:10
【问题描述】:

我试图了解一些我无法完全解释的有点“神奇”的行为的根本原因,这在阅读 ReactJS 源代码时并不明显。

当同步调用setState 方法以响应输入上的onChange 事件时,一切都按预期工作。输入的“新”值已经存在,因此 DOM 实际上并未更新。这是非常可取的,因为这意味着光标不会跳到输入框的末尾。

然而,当运行一个结构完全相同但调用setState异步的组件时,输入的“新”值似乎不存在,导致 ReactJS 实际触及DOM,这会导致光标跳到输入的末尾。

显然,在异步情况下,有些东西正在干预以将输入“重置”回其先前的value,而在同步情况下却没有这样做。这是什么机制?

同步示例

var synchronouslyUpdatingComponent =
    React.createFactory(React.createClass({
      getInitialState: function () {
        return {value: "Hello"};
      },

      changeHandler: function (e) {
        this.setState({value: e.target.value});
      },

      render: function () {
        var valueToSet = this.state.value;

        console.log("Rendering...");
        console.log("Setting value:" + valueToSet);
        if(this.isMounted()) {
            console.log("Current value:" + this.getDOMNode().value);
        }

        return React.DOM.input({value: valueToSet,
                                onChange: this.changeHandler});
    }
}));

请注意,代码将登录render方法,打印出实际DOM节点的当前value

当在“Hello”的两个 L 之间输入“X”时,我们会看到以下控制台输出,并且光标停留在预期的位置:

Rendering...
Setting value:HelXlo
Current value:HelXlo

异步示例

var asynchronouslyUpdatingComponent =
  React.createFactory(React.createClass({
    getInitialState: function () {
      return {value: "Hello"};
    },

    changeHandler: function (e) {
      var component = this;
      var value = e.target.value;
      window.setTimeout(function() {
        component.setState({value: value});
      });
    },

    render: function () {
      var valueToSet = this.state.value;

      console.log("Rendering...");
      console.log("Setting value:" + valueToSet);
      if(this.isMounted()) {
          console.log("Current value:" + this.getDOMNode().value);
      }

      return React.DOM.input({value: valueToSet,
                              onChange: this.changeHandler});
    }
}));

这与上面完全相同,只是对setState 的调用是在setTimeout 回调中。

在这种情况下,在两个 L 之间键入 X 会产生以下控制台输出,并且光标会跳到输入的末尾:

Rendering...
Setting value:HelXlo
Current value:Hello

这是为什么?

我理解 React 的 Controlled Component 概念,因此忽略用户对 value 的更改是有道理的。但看起来value 实际上已更改,然后显式重置。

显然,同步调用setState确保它在重置之前生效,而在任何其他时间调用setState发生在重置之后,强制重新-渲染。

这是真的吗?

JS Bin 示例

http://jsbin.com/sogunutoyi/1/

【问题讨论】:

  • 你能把这些变成两个 jsfiddle/jsbin/etc 的例子吗?这听起来像是 React 中的一个错误,但有工作代码来修补会很有用
  • @mike-pomax-kamermans 完成,已添加链接。

标签: javascript reactjs


【解决方案1】:

如前所述,在使用受控组件时这将是一个问题,因为 React 正在更新输入的值,而不是反之亦然(React 拦截更改请求并更新其状态以匹配)。

FakeRainBrigand 的回答很好,但我注意到更新是同步的还是异步的并不完全会导致输入以这种方式运行。如果您正在同步执行某些操作,例如应用掩码来修改返回值,它也可能导致光标跳到行尾。不幸的是(?)这正是 React 在受控输入方面的工作方式。但它可以手动解决。

在 react github 问题上有一个 great explanation and discussion of this,其中包括一个指向 JSBin solution 的链接 Sophie Alpert [手动确保光标保持在应有的位置]

这是使用<Input> 组件实现的,如下所示:

var Input = React.createClass({
  render: function() {
    return <input ref="root" {...this.props} value={undefined} />;
  },
  componentDidUpdate: function(prevProps) {
    var node = React.findDOMNode(this);
    var oldLength = node.value.length;
    var oldIdx = node.selectionStart;
    node.value = this.props.value;
    var newIdx = Math.max(0, node.value.length - oldLength + oldIdx);
    node.selectionStart = node.selectionEnd = newIdx;
  },
});

【讨论】:

    【解决方案2】:

    我们有类似的问题,在我们的例子中,我们必须使用异步状态更新。

    所以我们使用defaultValue,key 参数添加到与输入所反映的模型关联的输入。这可确保对于任何模型,输入将保持与模型同步,但如果实际模型更改将强制生成新输入。

    【讨论】:

      【解决方案3】:

      我在使用 Reflux 时遇到了同样的问题。状态存储在 React 组件之外,这导致与将 setState 包装在 setTimeout 内的效果类似。

      @dule 建议,我们应该让我们的状态变化同时同步和异步。所以我准备了一个 HOC 来确保值的变化是同步的 - 所以包装遭受异步状态变化的输入是很酷的。

      注意:此 HOC 仅适用于具有类似于 &lt;input/&gt; API 的组件,但我想如果有这样的需要,可以直接使其更通用。

      import React from 'react';
      import debounce from 'debounce';
      
      /**
       * The HOC solves a problem with cursor being moved to the end of input while typing.
       * This happens in case of controlled component, when setState part is executed asynchronously.
       * @param {string|React.Component} Component
       * @returns {SynchronousValueChanger}
       */
      const synchronousValueChangerHOC = function(Component) {
          class SynchronousValueChanger extends React.Component {
      
              static propTypes = {
                  onChange: React.PropTypes.func,
                  value: React.PropTypes.string
              };
      
              constructor(props) {
                  super(props);
                  this.state = {
                      value: props.value
                  };
              }
      
              propagateOnChange = debounce(e => {
                  this.props.onChange(e);
              }, onChangePropagationDelay);
      
              onChange = (e) => {
                  this.setState({value: e.target.value});
                  e.persist();
                  this.propagateOnChange(e);
              };
      
              componentWillReceiveProps(nextProps) {
                  if (nextProps.value !== this.state.value) {
                      this.setState({value: nextProps.value});
                  }
              }
      
              render() {
                  return <Component {...this.props} value={this.state.value} onChange={this.onChange}/>;
              }
          }
      
          return SynchronousValueChanger;
      };
      
      export default synchronousValueChangerHOC;
      
      const onChangePropagationDelay = 250;
      

      然后可以这样使用:

      const InputWithSynchronousValueChange = synchronousValueChangerHOC('input');
      

      通过使其成为 HOC,我们可以让它在输入、文本区域以及其他方面工作。也许这个名字不是最好的,所以如果你们中的任何人有改进的建议,请告诉我:)

      debounce 有一个 hack,因为有时,当输入非常快时,错误会再次出现。

      【讨论】:

      • 这太棒了。我测试了一个没有去抖动的版本,它似乎工作正常,即使它在几秒钟的疯狂随机输入后表现出轻微的延迟。不过为了安全起见,可能会使用去抖动。 (我的问题是未说明的,它也异步更新)。 PS:为简洁起见,我将其命名为 withSyncChange
      • 以防万一有人发生这种情况:我发布了一个更新 HOC 的要点(componentWillReceiveProps 现在已弃用):gist.github.com/mjsarfatti/6e1261f0be5f3c9eef5da1cda7bd3ffe
      【解决方案4】:

      使用 defaultValue 而不是 value 为我解决了这个问题。我不确定这是否是最好的解决方案,例如:

      发件人:

      return React.DOM.input({value: valueToSet,
          onChange: this.changeHandler});
      

      收件人:

      return React.DOM.input({defaultValue: valueToSet,
          onChange: this.changeHandler});
      

      JS Bin 示例

      http://jsbin.com/xusefuyucu/edit?js,output

      【讨论】:

      • 它确实解决了这个问题,但是你的输入变得不受控制,除了非常简单的用例,你迟早会遇到状态和显示不匹配的问题。
      【解决方案5】:

      这并不完全是一个答案,而是缓解问题的一种可能方法。它为 React 输入定义了一个包装器,通过本地状态 shim 同步管理值更新;并对传出值进行版本化,以便只应用从异步处理返回的最新值。

      它基于 Stephen Sugden (https://github.com/grncdr) 的一些工作,我针对现代 React 进行了更新,并通过对值进行版本控制进行了改进,从而消除了竞争条件。

      不漂亮:)

      http://jsfiddle.net/yrmmbjm1/1/

      var AsyncInput = asyncInput('input');
      

      这是组件需要如何使用它:

      var AI = asyncInput('input');
      
      var Test = React.createClass({
          // the controlling component must track
          // the version
          change: function(e, i) {
            var v = e.target.value;
            setTimeout(function() {
              this.setState({v: v, i: i});
            }.bind(this), Math.floor(Math.random() * 100 + 50));
          },
          getInitialState: function() { return {v: ''}; },
          render: function() {
            {/* and pass it down to the controlled input, yuck */}
            return <AI value={this.state.v} i={this.state.i} onChange={this.change} />
          }
      });
      React.render(<Test />, document.body);
      

      另一个试图减少对控制组件代码的影响的版本在这里:

      http://jsfiddle.net/yrmmbjm1/4/

      最终看起来像:

      var AI = asyncInput('input');
      
      var Test = React.createClass({
          // the controlling component must send versionedValues
          // back down to the input
          change: function(e) {
            var v = e.target.value;
            var f = e.valueFactory;
            setTimeout(function() {
              this.setState({v: f(v)});
            }.bind(this), Math.floor(Math.random() * 100 + 50));
          },
          getInitialState: function() { return {v: ''}; },
          render: function() {
            {/* and pass it down to the controlled input, yuck */}
            return <AI value={this.state.v} onChange={this.change} />
          }
      });
      React.render(<Test />, document.body);
      

      ¯\_(ツ)_/¯

      【讨论】:

        【解决方案6】:

        这就是正在发生的事情。

        同步

        • 你按 X
        • input.value 是 'HelXlo'
        • 你打电话给setState({value: 'HelXlo'})
        • 虚拟 dom 说输入值应该是 'HelXlo'
        • input.value 是 'HelXlo'
          • 未采取任何措施

        异步

        • 你按 X
        • input.value 是 'HelXlo'
        • 你什么都不做
        • 虚拟 DOM 说输入值应该是 'Hello'
          • react 将 input.value 设为“Hello”。

        稍后...

        • setState({value: 'HelXlo'})
        • 虚拟 DOM 说输入值应该是 'HelXlo'
          • react 使 input.value 'HelXlo'
          • 浏览器将光标跳到末尾(这是设置.value的副作用)

        魔法?

        是的,这里有一点魔法。 React 调用在事件处理程序之后同步呈现。这是避免闪烁所必需的。

        【讨论】:

        • 有没有不涉及手动光标位置管理的方法?
        • @Clever 一种可能适用于简单情况的解决方案是同时进行同步和异步。使用同步 setState 更新值,以便在处理异步时,虚拟 DOM 已经更新,因此不会更新输入字段,因此光标永远不会重置。
        • 这听起来好像没有真正的工作控制输入,因为只要真实 dom 和虚拟 dom 不匹配,光标就会跳到末尾。只要 dom 等于虚拟 dom,它就可以工作。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-03-31
        • 2020-04-22
        • 1970-01-01
        • 2018-06-16
        • 2015-12-09
        • 1970-01-01
        相关资源
        最近更新 更多