在 ReactJS 中,为什么“setState”在同步调用时表现不同?

IT技术 javascript reactjs
2021-04-05 05:00:36

我试图了解一些有点“神奇”的行为的根本原因,我发现我无法完全解释,并且从阅读 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方法,打印出value实际 DOM 节点的当前值。

在“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/

6个回答

这就是正在发生的事情。

同步

  • 你按 X
  • input.value 是 'HelXlo'
  • 你打电话 setState({value: 'HelXlo'})
  • 虚拟 dom 说输入值应该是“HelXlo”
  • input.value 是 'HelXlo'
    • 不采取行动

异步

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

稍后的...

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

魔法?

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

有没有不涉及手动光标位置管理的解决方法?
2021-06-02 05:00:36
@Clever 适用于简单情况的一种解决方案是同步和异步。使用同步setState更新值,以便在处理异步时,虚拟 DOM 已经更新,因此不会更新输入字段,因此永远不会重置光标。
2021-06-03 05:00:36
这听起来好像没有真正的工作受控输入,因为每当真实 dom 和虚拟 dom 不匹配时,光标就会跳到最后。只要 dom 等于虚拟 dom,它就可以工作。
2021-06-03 05:00:36

使用 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

它确实解决了问题,但您的输入变得不受控制,除了非常简单的用例,您迟早会遇到状态和所述状态的显示不匹配的问题。
2021-06-08 05:00:36

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

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

有一个的这个很好的解释和讨论在reactgithub上的问题,其中包括一个链接到一个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;
  },
});

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

它基于 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);

¯\_(ツ)_/¯

我在使用 Reflux 时遇到了同样的问题。状态存储在 React 组件之外,这与包装setStatesetTimeout.

@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,我们可以让它为输入、文本区域工作,也可能为其他人工作。也许这个名字不是最好的,所以如果你们中的任何人有如何改进的建议,请告诉我:)

有一个去抖动的黑客,因为有时,当打字真的很快完成时,错误会再次出现。

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