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

lev*_*and 47 javascript reactjs

我试图理解一些有些"神奇"行为的根本原因,我看到我无法完全解释,而且从阅读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});
    }
}));
Run Code Online (Sandbox Code Playgroud)

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

在两个Ls"Hello"之间键入"X"时,我们看到以下控制台输出,并且光标保持在预期的位置:

Rendering...
Setting value:HelXlo
Current value:HelXlo
Run Code Online (Sandbox Code Playgroud)

异步示例

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});
    }
}));
Run Code Online (Sandbox Code Playgroud)

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

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

Rendering...
Setting value:HelXlo
Current value:Hello
Run Code Online (Sandbox Code Playgroud)

为什么是这样?

我理解React的受控组件概念,因此value忽略用户对其的更改是有道理的.但看起来它value实际上已经改变了,然后明确地重置了.

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

事实上这是怎么回事?

JS Bin示例

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

Bri*_*and 76

这是正在发生的事情.

同步

  • 你按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调用在事件处理程序之后同步呈现.这是避免闪烁的必要条件.

  • @Clever一个可能适用于简单情况的解决方案是同步和异步.使用sync`setState`来更新该值,以便在处理异步时,虚拟DOM已经更新,因此不会更新输入字段,因此永远不会重置游标. (2认同)

Dan*_*ham 6

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

从:

return React.DOM.input({value: valueToSet,
    onChange: this.changeHandler});
Run Code Online (Sandbox Code Playgroud)

至:

return React.DOM.input({defaultValue: valueToSet,
    onChange: this.changeHandler});
Run Code Online (Sandbox Code Playgroud)

JS Bin示例

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

  • 它确实解决了问题,但是您的输入变得不受控制,除了非常简单的用例,您迟早会遇到状态和所述状态的显示不匹配的问题。 (2认同)

Dam*_*mon 5

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

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

有一个的这个很好的解释和讨论在反应github上的问题,其中包括一个链接到一个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;
  },
});
Run Code Online (Sandbox Code Playgroud)