javascript-在ReactJS中,为什么`setState`在同步调用时表现不同?

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

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

但是,当运行具有完全相同的结构但异步调用setState的组件时,输入的“新”值似乎不存在,从而导致ReactJS实际触摸DOM,这将导致光标跳至DOM的末尾。 输入。

显然,在异步情况下,正在采取某种措施将输入“重置”回其先前的setState,而在同步情况下则没有。 这个机械师是什么?

同步示例

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});
    }
}));

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

在“ 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});
    }
}));

除在2939202874460603603393回调中对setState的调用外,这与上面的完全相同。

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

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

为什么是这样?

我了解React的受控组件概念,因此忽略用户对setState的更改是有道理的。 但看起来setState实际上已更改,然后显式重置。

显然,同步调用setState可确保它在重置之前生效,而在重置后的任何其他时间调用setState都会发生,从而迫使重新渲染。

这实际上是怎么回事吗?

JS Bin示例

[HTTP://就是宾.com/so股nut O一/1/]

levand asked 2020-06-25T23:31:56Z
6个解决方案
75 votes

这是正在发生的事情。

同步

  • 你按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”
    • 反应使输入值``HelXlo''
    • 浏览器将光标跳到末尾(这是设置.value的副作用)

魔法?

是的,这里有些魔术。 在事件处理程序之后,React调用将同步呈现。 这是避免闪烁的必要条件。

Brigand answered 2020-06-25T23:33:31Z
6 votes

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

从:

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

至:

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

JS Bin示例

[HTTP://就是宾.com/徐色敷腴粗/edit?就是,output]

Daniel Billingham answered 2020-06-25T23:34:08Z
4 votes

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

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

在react github问题上对此进行了很好的解释和讨论,其中包括Sophie Alpert的JSBin解决方案的链接[可手动确保光标停留在应有的位置]

这是使用<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;
  },
});
Damon answered 2020-06-25T23:34:42Z
3 votes

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

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

它不漂亮:)

[HTTP://JS fiddle.net/有人明码标价卖1/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://JS fiddle.net/有人明码标价卖1/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);

¯\_(ツ)_/¯

Eric O'Connell answered 2020-06-25T23:35:38Z
1 votes

使用Reflux时我遇到了同样的问题。 状态存储在React Component外部,其产生的效果类似于将<input/>包裹在setTimeout内部。

@dule建议,我们应该同时使状态更改同步和异步。 因此,我准备了一个HOC,以确保值更改是同步的-因此包装遭受异步状态更改的输入很酷。

注意:此HOC仅适用于具有类似于<input/> 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,我们可以使其适用于输入,文本区域,也可能适用于其他区域。 也许这个名字不是最好的,所以如果你们中有人对如何改进提出建议,请告诉我:)

有时会出现反跳现象,因为有时打字时很快就会出现该错误。

chmurson answered 2020-06-25T23:36:21Z
-1 votes

我们有一个类似的问题,在这种情况下,我们必须使用异步状态更新。

因此,我们使用defaultValue,并向与该输入所反映的模型相关联的输入中添加一个2939208796566193193152参数。 这样可以确保对于任何模型,输入都将与模型保持同步,但是如果实际模型发生更改,将强制生成新的输入。

Mitch VanDuyn answered 2020-06-25T23:36:46Z
translate from https://stackoverflow.com:/questions/28922275/in-reactjs-why-does-setstate-behave-differently-when-called-synchronously